@blamejs/blamejs-shop 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +49 -45
- package/lib/affiliates.js +7 -23
- package/lib/announcement-bar.js +5 -22
- package/lib/api-keys.js +4 -13
- package/lib/asset-manifest.json +1 -1
- package/lib/banner-ab-tests.js +5 -9
- package/lib/blog-articles.js +8 -26
- package/lib/captcha-gate.js +7 -17
- package/lib/carrier-accounts.js +3 -10
- package/lib/catalog-drafts.js +5 -12
- package/lib/category-navigation.js +4 -20
- package/lib/cms-blocks.js +4 -7
- package/lib/compliance-export.js +4 -11
- package/lib/consent-ledger.js +2 -4
- package/lib/cost-layers.js +56 -21
- package/lib/customer-import.js +2 -2
- package/lib/customer-notes.js +7 -23
- package/lib/customer-roles.js +3 -5
- package/lib/customer-segments.js +17 -14
- package/lib/customer-surveys.js +7 -14
- package/lib/customers.js +2 -4
- package/lib/dispute-resolution.js +5 -13
- package/lib/dropship-forwarding.js +3 -5
- package/lib/dunning.js +2 -7
- package/lib/email-ab-tests.js +4 -8
- package/lib/email-campaigns.js +25 -14
- package/lib/email-warmup.js +2 -7
- package/lib/experiments.js +6 -10
- package/lib/externaldb-d1.js +78 -44
- package/lib/geolocation.js +18 -28
- package/lib/gift-options.js +5 -23
- package/lib/knowledge-base.js +7 -22
- package/lib/line-gift-wrap.js +3 -17
- package/lib/live-chat.js +5 -18
- package/lib/metered-usage.js +3 -8
- package/lib/notifications.js +3 -7
- package/lib/operator-accounts.js +3 -5
- package/lib/operator-approvals.js +6 -8
- package/lib/operator-audit-log.js +2 -7
- package/lib/operator-help-center.js +5 -21
- package/lib/operator-inbox.js +4 -11
- package/lib/operator-roles.js +5 -7
- package/lib/order-escalation.js +2 -2
- package/lib/order-export.js +24 -20
- package/lib/order-notes.js +4 -14
- package/lib/order-ratings.js +4 -10
- package/lib/payment-methods.js +5 -13
- package/lib/payment-retries.js +2 -7
- package/lib/payment.js +103 -58
- package/lib/pixel-events.js +8 -23
- package/lib/plan-changes.js +2 -7
- package/lib/product-qa.js +4 -8
- package/lib/promo-banners.js +7 -15
- package/lib/purchase-orders.js +3 -6
- package/lib/push-notifications.js +7 -32
- package/lib/pwa-manifest.js +6 -11
- package/lib/quotes.js +5 -9
- package/lib/referrals.js +10 -6
- package/lib/reviews.js +4 -10
- package/lib/robots-config.js +6 -14
- package/lib/sales-tax-filings.js +2 -2
- package/lib/search-facets.js +5 -5
- package/lib/search-ranking.js +4 -7
- package/lib/search-synonyms.js +7 -21
- package/lib/security-middleware.js +125 -70
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +49 -137
- package/lib/sitemap-generator.js +3 -7
- package/lib/sms-dispatcher.js +7 -23
- package/lib/stock-receipts.js +4 -10
- package/lib/storefront-dashboards.js +2 -10
- package/lib/storefront-forms.js +9 -22
- package/lib/storefront-pages.js +6 -20
- package/lib/storefront.js +21 -23
- package/lib/subscription-billing.js +3 -8
- package/lib/subscription-controls.js +2 -7
- package/lib/suggestion-box.js +9 -26
- package/lib/support-tickets.js +6 -24
- package/lib/tax-remittance.js +2 -2
- package/lib/tenants.js +2 -7
- package/lib/text-guard.js +137 -35
- package/lib/theme-assets.js +4 -16
- package/lib/translations.js +3 -6
- package/lib/trust-badges.js +4 -7
- package/lib/vendor/MANIFEST.json +107 -97
- package/lib/vendor/blamejs/CHANGELOG.md +180 -0
- package/lib/vendor/blamejs/NOTICE +14 -10
- package/lib/vendor/blamejs/README.md +17 -5
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/acme.js +9 -9
- package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
- package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
- package/lib/vendor/blamejs/lib/archive.js +13 -24
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +5 -5
- package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
- package/lib/vendor/blamejs/lib/constants.js +79 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
- package/lib/vendor/blamejs/lib/error-page.js +20 -1
- package/lib/vendor/blamejs/lib/external-db.js +5 -1
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
- package/lib/vendor/blamejs/lib/flag.js +28 -2
- package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
- package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
- package/lib/vendor/blamejs/lib/http-client.js +217 -40
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
- package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
- package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
- package/lib/vendor/blamejs/lib/mail-store.js +7 -1
- package/lib/vendor/blamejs/lib/mail.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +3 -3
- package/lib/vendor/blamejs/lib/network-nts.js +35 -14
- package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +341 -11
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
- package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
- package/lib/vendor/blamejs/lib/openapi.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
- package/lib/vendor/blamejs/lib/problem-details.js +2 -1
- package/lib/vendor/blamejs/lib/redis-client.js +12 -1
- package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
- package/lib/vendor/blamejs/lib/render.js +531 -7
- package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
- package/lib/vendor/blamejs/lib/router.js +37 -13
- package/lib/vendor/blamejs/lib/safe-async.js +142 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/testing.js +54 -7
- package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
- package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
- package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
- package/lib/vendor/blamejs/lib/webhook.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +24 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/sbom.cdx.json +6 -6
- package/lib/vendor-invoices.js +3 -6
- package/lib/vendors.js +5 -19
- package/lib/webhook-receiver.js +46 -83
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- package/package.json +1 -1
package/lib/quotes.js
CHANGED
|
@@ -145,11 +145,6 @@ var VIEW_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
|
145
145
|
// are allowed inside multi-line free-text fields (message,
|
|
146
146
|
// operator_notes, line.notes); single-line fields (terms, reject /
|
|
147
147
|
// cancel reason, accepted_by) refuse them too.
|
|
148
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
149
|
-
var CONTROL_BYTE_MULTILINE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
150
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
151
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
152
|
-
);
|
|
153
148
|
|
|
154
149
|
var QUOTE_STATUSES = Object.freeze([
|
|
155
150
|
"requested", "responded", "accepted", "rejected",
|
|
@@ -160,6 +155,7 @@ var TERMINAL_STATUSES = Object.freeze([
|
|
|
160
155
|
]);
|
|
161
156
|
|
|
162
157
|
var b = require("./vendor/blamejs");
|
|
158
|
+
var textGuard = require("./text-guard");
|
|
163
159
|
|
|
164
160
|
// The quote lifecycle, modelled on b.fsm — the same composition the order
|
|
165
161
|
// primitive uses (lib/order.js#_getOrderFsm). The FSM is the single source of
|
|
@@ -316,10 +312,10 @@ function _optShortText(s, label, max) {
|
|
|
316
312
|
if (s.length > max) {
|
|
317
313
|
throw new TypeError("quotes: " + label + " must be <= " + max + " characters");
|
|
318
314
|
}
|
|
319
|
-
if (
|
|
315
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
320
316
|
throw new TypeError("quotes: " + label + " contains control bytes");
|
|
321
317
|
}
|
|
322
|
-
if (
|
|
318
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
323
319
|
throw new TypeError("quotes: " + label + " contains zero-width / direction-override bytes");
|
|
324
320
|
}
|
|
325
321
|
return s;
|
|
@@ -336,10 +332,10 @@ function _optLongText(s, label, max) {
|
|
|
336
332
|
if (s.length > max) {
|
|
337
333
|
throw new TypeError("quotes: " + label + " must be <= " + max + " characters");
|
|
338
334
|
}
|
|
339
|
-
if (
|
|
335
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
340
336
|
throw new TypeError("quotes: " + label + " contains control bytes");
|
|
341
337
|
}
|
|
342
|
-
if (
|
|
338
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
343
339
|
throw new TypeError("quotes: " + label + " contains zero-width / direction-override bytes");
|
|
344
340
|
}
|
|
345
341
|
return s;
|
package/lib/referrals.js
CHANGED
|
@@ -161,13 +161,17 @@ function _alphabetRe(alphabet) {
|
|
|
161
161
|
// default alphabet has none, but an operator-supplied 32-char set
|
|
162
162
|
// could legally include `]` or `-`.
|
|
163
163
|
var escaped = alphabet.replace(/[\\\]^-]/g, "\\$&");
|
|
164
|
-
|
|
164
|
+
var source = "^[" + escaped + "]+$";
|
|
165
|
+
// A single anchored character class is structurally incapable of
|
|
165
166
|
// catastrophic backtracking — one unbounded quantifier over a class, no
|
|
166
|
-
// grouped repetition
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
167
|
+
// grouped repetition. The screen still runs: the class body is built from an
|
|
168
|
+
// operator-supplied alphabet, so it is the escaping above, not the shape,
|
|
169
|
+
// that keeps this pattern to one class. If a character ever escapes that
|
|
170
|
+
// replace and closes the class early, the result stops being the safe shape
|
|
171
|
+
// this reasoning depends on, and the screen catches it at config time rather
|
|
172
|
+
// than at match time.
|
|
173
|
+
b.guardRegex.assertSafe(source);
|
|
174
|
+
return new RegExp(source);
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
// ---- factory ------------------------------------------------------------
|
package/lib/reviews.js
CHANGED
|
@@ -50,9 +50,9 @@ var REVIEW_ORDER_KEY = ["created_at:desc", "id:desc"];
|
|
|
50
50
|
var CUSTOMER_ORDER_KEY = ["created_at:desc", "id:desc"];
|
|
51
51
|
|
|
52
52
|
var ALLOWED_STATUSES = ["pending", "published", "rejected"];
|
|
53
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
54
53
|
|
|
55
54
|
var b = require("./vendor/blamejs");
|
|
55
|
+
var textGuard = require("./text-guard");
|
|
56
56
|
|
|
57
57
|
// ---- validators ---------------------------------------------------------
|
|
58
58
|
|
|
@@ -75,9 +75,7 @@ function _title(s) {
|
|
|
75
75
|
if (s.length > MAX_TITLE_LEN) {
|
|
76
76
|
throw new TypeError("reviews: title must be <= " + MAX_TITLE_LEN + " characters");
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
throw new TypeError("reviews: title contains control bytes");
|
|
80
|
-
}
|
|
78
|
+
textGuard.freeText(s, "reviews: title");
|
|
81
79
|
return s;
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -89,9 +87,7 @@ function _body(s) {
|
|
|
89
87
|
if (s.length > MAX_BODY_LEN) {
|
|
90
88
|
throw new TypeError("reviews: body must be <= " + MAX_BODY_LEN + " characters");
|
|
91
89
|
}
|
|
92
|
-
|
|
93
|
-
throw new TypeError("reviews: body contains control bytes");
|
|
94
|
-
}
|
|
90
|
+
textGuard.freeText(s, "reviews: body");
|
|
95
91
|
return s;
|
|
96
92
|
}
|
|
97
93
|
|
|
@@ -288,9 +284,7 @@ function create(opts) {
|
|
|
288
284
|
if (reason.length > MAX_REJECT_REASON_LEN) {
|
|
289
285
|
throw new TypeError("reviews.reject: reason must be <= " + MAX_REJECT_REASON_LEN + " characters");
|
|
290
286
|
}
|
|
291
|
-
|
|
292
|
-
throw new TypeError("reviews.reject: reason contains control bytes");
|
|
293
|
-
}
|
|
287
|
+
textGuard.freeText(reason, "reviews.reject: reason");
|
|
294
288
|
}
|
|
295
289
|
var ts = _now();
|
|
296
290
|
var existing = (await query("SELECT id, status FROM reviews WHERE id = ?1", [id])).rows[0];
|
package/lib/robots-config.js
CHANGED
|
@@ -96,9 +96,9 @@ var MAX_CRAWL_DELAY = 86400; // 24 h — anything larger is a typo
|
|
|
96
96
|
var MAX_PRIORITY = 1000000;
|
|
97
97
|
var MAX_RULE_ID_LEN = 80;
|
|
98
98
|
|
|
99
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
100
99
|
|
|
101
100
|
var b = require("./vendor/blamejs");
|
|
101
|
+
var textGuard = require("./text-guard");
|
|
102
102
|
|
|
103
103
|
// ---- predefined templates ---------------------------------------------
|
|
104
104
|
//
|
|
@@ -183,9 +183,7 @@ function _userAgent(s) {
|
|
|
183
183
|
"robotsConfig: user_agent must be a string 1.." + MAX_USER_AGENT_LEN + " chars"
|
|
184
184
|
);
|
|
185
185
|
}
|
|
186
|
-
|
|
187
|
-
throw new TypeError("robotsConfig: user_agent must not contain control bytes");
|
|
188
|
-
}
|
|
186
|
+
textGuard.freeText(s, "robotsConfig: user_agent", { singleLine: "reject" });
|
|
189
187
|
// robots.txt is a colon-delimited key/value format — a colon in the
|
|
190
188
|
// user-agent value would shift the parser onto a different key.
|
|
191
189
|
if (s.indexOf(":") !== -1) {
|
|
@@ -200,7 +198,7 @@ function _pathEntry(s, label) {
|
|
|
200
198
|
"robotsConfig: " + label + " entry must be a string 1.." + MAX_PATH_LEN + " chars"
|
|
201
199
|
);
|
|
202
200
|
}
|
|
203
|
-
if (
|
|
201
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
204
202
|
throw new TypeError("robotsConfig: " + label + " entry must not contain control bytes");
|
|
205
203
|
}
|
|
206
204
|
// robots.txt path entries are /-rooted absolute paths. Wildcards
|
|
@@ -258,9 +256,7 @@ function _ruleId(s) {
|
|
|
258
256
|
"robotsConfig: rule_id must be a string 1.." + MAX_RULE_ID_LEN + " chars"
|
|
259
257
|
);
|
|
260
258
|
}
|
|
261
|
-
|
|
262
|
-
throw new TypeError("robotsConfig: rule_id must not contain control bytes");
|
|
263
|
-
}
|
|
259
|
+
textGuard.freeText(s, "robotsConfig: rule_id", { singleLine: "reject" });
|
|
264
260
|
return s;
|
|
265
261
|
}
|
|
266
262
|
|
|
@@ -270,9 +266,7 @@ function _sitemapUrl(u) {
|
|
|
270
266
|
"robotsConfig: sitemap url must be a string 1.." + MAX_SITEMAP_URL_LEN + " chars"
|
|
271
267
|
);
|
|
272
268
|
}
|
|
273
|
-
|
|
274
|
-
throw new TypeError("robotsConfig: sitemap url must not contain control bytes");
|
|
275
|
-
}
|
|
269
|
+
textGuard.freeText(u, "robotsConfig: sitemap url", { singleLine: "reject" });
|
|
276
270
|
// robots.txt only carries the URL itself, so the path-rooted form
|
|
277
271
|
// promo-banners accepts isn't meaningful here — every crawler treats
|
|
278
272
|
// the Sitemap: value as an absolute URL. https-only protects against
|
|
@@ -293,9 +287,7 @@ function _hostDirective(s) {
|
|
|
293
287
|
"robotsConfig: host must be a string 1.." + MAX_HOST_LEN + " chars"
|
|
294
288
|
);
|
|
295
289
|
}
|
|
296
|
-
|
|
297
|
-
throw new TypeError("robotsConfig: host must not contain control bytes");
|
|
298
|
-
}
|
|
290
|
+
textGuard.freeText(s, "robotsConfig: host", { singleLine: "reject" });
|
|
299
291
|
// The host directive is a bare hostname (optionally with :port) —
|
|
300
292
|
// no scheme, no path. Refuse anything that looks like a URL so an
|
|
301
293
|
// operator can't accidentally paste the full origin URL in.
|
package/lib/sales-tax-filings.js
CHANGED
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
*/
|
|
105
105
|
|
|
106
106
|
var b = require("./vendor/blamejs");
|
|
107
|
+
var textGuard = require("./text-guard");
|
|
107
108
|
var C = b.constants;
|
|
108
109
|
|
|
109
110
|
// ---- constants ----------------------------------------------------------
|
|
@@ -122,7 +123,6 @@ var DEFAULT_LIMIT = 50;
|
|
|
122
123
|
var MAX_LIMIT = 500;
|
|
123
124
|
var MAX_DAYS_AHEAD = 365;
|
|
124
125
|
|
|
125
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
126
126
|
|
|
127
127
|
// FSM transition map. The keys are (from_status, action) → to_status.
|
|
128
128
|
// Defined as a table so a misuse surfaces with the actual current
|
|
@@ -211,7 +211,7 @@ function _shortText(s, label, max) {
|
|
|
211
211
|
throw new TypeError("salesTaxFilings: " + label +
|
|
212
212
|
" must be a non-empty string <= " + max + " chars");
|
|
213
213
|
}
|
|
214
|
-
if (
|
|
214
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
215
215
|
throw new TypeError("salesTaxFilings: " + label + " must not contain control bytes");
|
|
216
216
|
}
|
|
217
217
|
return s;
|
package/lib/search-facets.js
CHANGED
|
@@ -106,7 +106,6 @@ var ALLOWED_KINDS = ["categorical", "numeric_range", "boolean"];
|
|
|
106
106
|
var ALLOWED_COLS = ["field", "kind", "buckets_json", "display_limit", "active"];
|
|
107
107
|
|
|
108
108
|
// Refuse C0 control bytes + DEL on every operator-supplied string.
|
|
109
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
110
109
|
|
|
111
110
|
// Operator-chosen identifier shape — lowercase alnum + hyphen +
|
|
112
111
|
// underscore, must start with a letter. Same shape the rest of the
|
|
@@ -118,6 +117,7 @@ var KEY_RE = /^[a-z][a-z0-9_-]*$/;
|
|
|
118
117
|
var FIELD_RE = /^[a-z][a-z0-9_-]*$/;
|
|
119
118
|
|
|
120
119
|
var b = require("./vendor/blamejs");
|
|
120
|
+
var textGuard = require("./text-guard");
|
|
121
121
|
|
|
122
122
|
// ---- validators ---------------------------------------------------------
|
|
123
123
|
|
|
@@ -175,7 +175,7 @@ function _label(s, fnLabel) {
|
|
|
175
175
|
if (s.length > MAX_LABEL_LEN) {
|
|
176
176
|
throw new TypeError(fnLabel + ": bucket label must be <= " + MAX_LABEL_LEN + " characters");
|
|
177
177
|
}
|
|
178
|
-
if (
|
|
178
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
179
179
|
throw new TypeError(fnLabel + ": bucket label must not contain control bytes");
|
|
180
180
|
}
|
|
181
181
|
return s;
|
|
@@ -291,7 +291,7 @@ function _appliedFilters(input, fnLabel) {
|
|
|
291
291
|
if (s.length > MAX_VALUE_LEN) {
|
|
292
292
|
throw new TypeError(fnLabel + ": applied_filters['" + k + "'][" + j + "] must be <= " + MAX_VALUE_LEN + " characters");
|
|
293
293
|
}
|
|
294
|
-
if (
|
|
294
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
295
295
|
throw new TypeError(fnLabel + ": applied_filters['" + k + "'][" + j + "] must not contain control bytes");
|
|
296
296
|
}
|
|
297
297
|
values.push(s);
|
|
@@ -318,7 +318,7 @@ function _value(raw, fnLabel) {
|
|
|
318
318
|
if (s.length > MAX_VALUE_LEN) {
|
|
319
319
|
throw new TypeError(fnLabel + ": value must be <= " + MAX_VALUE_LEN + " characters");
|
|
320
320
|
}
|
|
321
|
-
if (
|
|
321
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
322
322
|
throw new TypeError(fnLabel + ": value must not contain control bytes");
|
|
323
323
|
}
|
|
324
324
|
return s;
|
|
@@ -331,7 +331,7 @@ function _sessionId(s, fnLabel) {
|
|
|
331
331
|
if (s.length > 256) {
|
|
332
332
|
throw new TypeError(fnLabel + ": session_id must be <= 256 characters");
|
|
333
333
|
}
|
|
334
|
-
if (
|
|
334
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
335
335
|
throw new TypeError(fnLabel + ": session_id must not contain control bytes");
|
|
336
336
|
}
|
|
337
337
|
return s;
|
package/lib/search-ranking.js
CHANGED
|
@@ -121,12 +121,9 @@ var SIGNAL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
|
121
121
|
// which covers SKU notation across every common upstream system
|
|
122
122
|
// without opening the door to control bytes or whitespace.
|
|
123
123
|
var PRODUCT_ID_RE = /^[A-Za-z0-9._:/-]+$/;
|
|
124
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
125
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
126
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
127
|
-
);
|
|
128
124
|
|
|
129
125
|
var b = require("./vendor/blamejs");
|
|
126
|
+
var textGuard = require("./text-guard");
|
|
130
127
|
|
|
131
128
|
// ---- monotonic clock ---------------------------------------------------
|
|
132
129
|
//
|
|
@@ -175,7 +172,7 @@ function _name(s, fnLabel) {
|
|
|
175
172
|
if (s.length > MAX_NAME_LEN) {
|
|
176
173
|
throw new TypeError(fnLabel + ": name must be <= " + MAX_NAME_LEN + " characters");
|
|
177
174
|
}
|
|
178
|
-
if (
|
|
175
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
179
176
|
throw new TypeError(fnLabel + ": name contains control / zero-width bytes");
|
|
180
177
|
}
|
|
181
178
|
return s;
|
|
@@ -226,7 +223,7 @@ function _normalizeQuery(s, fnLabel) {
|
|
|
226
223
|
if (s.length > MAX_QUERY_LEN) {
|
|
227
224
|
throw new TypeError(fnLabel + ": query must be <= " + MAX_QUERY_LEN + " characters");
|
|
228
225
|
}
|
|
229
|
-
if (
|
|
226
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
230
227
|
throw new TypeError(fnLabel + ": query contains control / zero-width bytes");
|
|
231
228
|
}
|
|
232
229
|
var normalized = s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
@@ -270,7 +267,7 @@ function _sessionIdRaw(s, fnLabel) {
|
|
|
270
267
|
if (s.length > MAX_SESSION_ID_LEN) {
|
|
271
268
|
throw new TypeError(fnLabel + ": session_id must be <= " + MAX_SESSION_ID_LEN + " characters");
|
|
272
269
|
}
|
|
273
|
-
if (
|
|
270
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
274
271
|
throw new TypeError(fnLabel + ": session_id contains control / zero-width bytes");
|
|
275
272
|
}
|
|
276
273
|
return s;
|
package/lib/search-synonyms.js
CHANGED
|
@@ -102,18 +102,6 @@ var MAX_QUERY_LEN = 500;
|
|
|
102
102
|
var ALLOWED_KINDS = ["bidirectional", "directional"];
|
|
103
103
|
var ALLOWED_GROUP_COLS = ["kind", "terms_json"];
|
|
104
104
|
|
|
105
|
-
// Refuse C0 control bytes + DEL. Tokens are single-line search terms;
|
|
106
|
-
// any newline / tab in a synonym entry would either be malformed
|
|
107
|
-
// operator input or an attempt to smuggle separator-confusion payload
|
|
108
|
-
// into the search log.
|
|
109
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
110
|
-
// Zero-width + direction-override family — same defense as
|
|
111
|
-
// `supportTickets`. Spelled with \u escapes so the file stays
|
|
112
|
-
// ESLint no-irregular-whitespace clean.
|
|
113
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
114
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
115
|
-
);
|
|
116
|
-
|
|
117
105
|
// Slug shape — operator-chosen identifier on synonym groups. Same
|
|
118
106
|
// shape the rest of the shop uses: lowercase alnum + hyphen, must
|
|
119
107
|
// start with a letter, 1..MAX_SLUG_LEN.
|
|
@@ -126,6 +114,7 @@ var TOKEN_SPLIT_RE = /[^a-z0-9-]+/g;
|
|
|
126
114
|
|
|
127
115
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
128
116
|
var b = require("./vendor/blamejs");
|
|
117
|
+
var textGuard = require("./text-guard");
|
|
129
118
|
|
|
130
119
|
// ---- validators ---------------------------------------------------------
|
|
131
120
|
|
|
@@ -166,10 +155,10 @@ function _term(s, label, fnLabel) {
|
|
|
166
155
|
if (trimmed.length > MAX_TERM_LEN) {
|
|
167
156
|
throw new TypeError(fnLabel + ": " + label + " must be <= " + MAX_TERM_LEN + " characters");
|
|
168
157
|
}
|
|
169
|
-
if (
|
|
158
|
+
if (textGuard.hasCodepointThreat(trimmed, { singleLine: "reject" })) {
|
|
170
159
|
throw new TypeError(fnLabel + ": " + label + " contains control bytes");
|
|
171
160
|
}
|
|
172
|
-
if (
|
|
161
|
+
if (textGuard.hasCodepointThreat(trimmed, { zeroWidth: "reject" })) {
|
|
173
162
|
throw new TypeError(fnLabel + ": " + label + " contains zero-width / direction-override characters");
|
|
174
163
|
}
|
|
175
164
|
return trimmed;
|
|
@@ -247,13 +236,10 @@ function _queryString(s, fnLabel) {
|
|
|
247
236
|
// give attackers a denial-of-search vector against legitimate
|
|
248
237
|
// shoppers who happened to paste a stray BOM.
|
|
249
238
|
function _sanitiseQuery(s) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]", "g"),
|
|
255
|
-
""
|
|
256
|
-
);
|
|
239
|
+
// Controls become a SPACE, not nothing: deleting them outright would weld
|
|
240
|
+
// the words on either side into one token. The invisible characters do get
|
|
241
|
+
// deleted — they sit inside a word rather than between two.
|
|
242
|
+
return textGuard.scrubInvisible(s.replace(/[\x00-\x1f\x7f]/g, " "));
|
|
257
243
|
}
|
|
258
244
|
|
|
259
245
|
function _tokenise(s) {
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
* visitor in ONE bucket behind the fabric and let a single global limit
|
|
39
39
|
* throttle the whole store.
|
|
40
40
|
*
|
|
41
|
+
* That read goes through `b.requestHelpers.trustedClientIp`, which honours
|
|
42
|
+
* the forwarded header only when the socket peer sits inside the trusted
|
|
43
|
+
* range (see TRUSTED_PROXY_CIDRS) — a header alone never speaks for a client.
|
|
44
|
+
*
|
|
41
45
|
* The container runs as a single instance (max_instances=1), so the
|
|
42
46
|
* default in-memory rate-limit backend is correct — there is no second
|
|
43
47
|
* replica to coordinate a shared counter with, and the in-memory token
|
|
@@ -274,12 +278,35 @@ function isEdgeExemptPath(pathname) {
|
|
|
274
278
|
var PUBLIC_ORIGINS = (process.env.SHOP_PUBLIC_ORIGINS || "https://blamejs.shop")
|
|
275
279
|
.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
276
280
|
|
|
281
|
+
// Which socket peers may speak for a client other than themselves.
|
|
282
|
+
//
|
|
283
|
+
// Same topology as resolveProtocol below: the container has no public ingress
|
|
284
|
+
// and is reachable only through the Cloudflare Worker over the service
|
|
285
|
+
// binding, so every peer that can open a socket here IS the edge — which is
|
|
286
|
+
// what the all-addresses default states. It is written as a CIDR list rather
|
|
287
|
+
// than an unconditional "believe the header" so that a deployment which does
|
|
288
|
+
// expose the container directly has one value to narrow, and so the gate is
|
|
289
|
+
// the framework's rather than a second trust model living here.
|
|
290
|
+
//
|
|
291
|
+
// Narrowing it is the right move the moment the container is reachable any
|
|
292
|
+
// other way: an unlisted peer's forwarded headers are ignored and keying falls
|
|
293
|
+
// back to that peer's own address.
|
|
294
|
+
var TRUSTED_PROXY_CIDRS = (process.env.SHOP_TRUSTED_PROXIES || "0.0.0.0/0,::/0")
|
|
295
|
+
.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
296
|
+
|
|
297
|
+
// The edge publishes the client address as `cf-connecting-ip` and overwrites
|
|
298
|
+
// any value a client sent under that name. `x-real-ip` is the usual nginx
|
|
299
|
+
// spelling and `x-forwarded-for` the multi-hop standard, kept for a
|
|
300
|
+
// self-hosted deployment behind either. First header PRESENT wins, so an empty
|
|
301
|
+
// leading header means "this request carries no forwarded address" rather than
|
|
302
|
+
// deferring to one further down the list that a client may have set.
|
|
303
|
+
var _trustedClientIp = b.requestHelpers.trustedClientIp({
|
|
304
|
+
trustedProxies: TRUSTED_PROXY_CIDRS,
|
|
305
|
+
forwardedHeaders: ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"],
|
|
306
|
+
});
|
|
307
|
+
|
|
277
308
|
/**
|
|
278
|
-
* Resolve the real client IP for rate-limit keying.
|
|
279
|
-
* Cloudflare-injected `cf-connecting-ip` first (the canonical single
|
|
280
|
-
* client address behind the fabric), then `x-real-ip` (its mirror),
|
|
281
|
-
* then falls back to the socket address via `b.requestHelpers.clientIp`
|
|
282
|
-
* for direct connections (e2e harness, local dev). Always returns a
|
|
309
|
+
* Resolve the real client IP for rate-limit keying. Always returns a
|
|
283
310
|
* non-empty string so two un-identifiable clients never collapse into
|
|
284
311
|
* the same bucket as a real IP — request-shape reader, returns a
|
|
285
312
|
* default, never throws.
|
|
@@ -292,15 +319,15 @@ var PUBLIC_ORIGINS = (process.env.SHOP_PUBLIC_ORIGINS || "https://blamejs.shop")
|
|
|
292
319
|
* source on every request and mint unlimited buckets — walking every
|
|
293
320
|
* per-IP limiter (and the captcha-IP budget) that keys off this value.
|
|
294
321
|
* Keying on the /64 closes that while still distinguishing real end-sites.
|
|
322
|
+
*
|
|
323
|
+
* Resolution itself is `b.requestHelpers.trustedClientIp`, which reads the
|
|
324
|
+
* header family named below, folds an IPv4-mapped IPv6 peer, walks a multi-hop
|
|
325
|
+
* value right-to-left, and falls back to the socket address — one implementation
|
|
326
|
+
* shared with every other gate rather than a second, looser one here.
|
|
295
327
|
*/
|
|
296
328
|
function clientKey(req) {
|
|
297
|
-
var
|
|
298
|
-
|
|
299
|
-
if (typeof cf === "string" && cf.length > 0) return _ipBucket(cf.trim());
|
|
300
|
-
var real = headers["x-real-ip"];
|
|
301
|
-
if (typeof real === "string" && real.length > 0) return _ipBucket(real.trim());
|
|
302
|
-
var sock = b.requestHelpers.clientIp(req);
|
|
303
|
-
return sock ? _ipBucket(sock) : "unknown";
|
|
329
|
+
var ip = _trustedClientIp.resolve(req);
|
|
330
|
+
return ip ? _ipBucket(ip) : "unknown";
|
|
304
331
|
}
|
|
305
332
|
|
|
306
333
|
// Collapse one resolved client IP to its per-IP bucket key. ipKey returns
|
|
@@ -500,26 +527,6 @@ var _SCOPED_DIRECTIVES = {
|
|
|
500
527
|
// default-src 'self', object-src 'none',
|
|
501
528
|
// require-trusted-types-for 'script', frame-ancestors 'none', base-uri 'self'
|
|
502
529
|
|
|
503
|
-
// Parse the vendored default CSP into an ordered directive map ONCE at
|
|
504
|
-
// module load. The default is a `name a b; name c;` string; split on `;`,
|
|
505
|
-
// the first token of each clause is the directive name, the rest its
|
|
506
|
-
// source list.
|
|
507
|
-
function _parseCsp(cspString) {
|
|
508
|
-
var order = [];
|
|
509
|
-
var map = {};
|
|
510
|
-
String(cspString).split(";").forEach(function (clause) {
|
|
511
|
-
var trimmed = clause.trim();
|
|
512
|
-
if (!trimmed) return;
|
|
513
|
-
var parts = trimmed.split(/\s+/);
|
|
514
|
-
var name = parts[0];
|
|
515
|
-
order.push(name);
|
|
516
|
-
map[name] = parts.slice(1);
|
|
517
|
-
});
|
|
518
|
-
return { order: order, map: map };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
var _DEFAULT_CSP_PARSED = _parseCsp(_vendoredSecurityHeaders.DEFAULT_CSP);
|
|
522
|
-
|
|
523
530
|
/**
|
|
524
531
|
* Build a route-scoped CSP string from the vendored strict default plus the
|
|
525
532
|
* named providers' hosts. `keys` is an array of CSP_HOSTS keys
|
|
@@ -538,40 +545,51 @@ var _DEFAULT_CSP_PARSED = _parseCsp(_vendoredSecurityHeaders.DEFAULT_CSP);
|
|
|
538
545
|
* which fails safe (the SDK is refused, not admitted too broadly).
|
|
539
546
|
*/
|
|
540
547
|
function scopedCsp(keys) {
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
var
|
|
545
|
-
_DEFAULT_CSP_PARSED.order.forEach(function (name) {
|
|
546
|
-
map[name] = _DEFAULT_CSP_PARSED.map[name].slice();
|
|
547
|
-
});
|
|
548
|
-
|
|
548
|
+
// Collect the hosts each named provider needs, per directive. Unknown keys
|
|
549
|
+
// fall out here, which is what keeps a mistyped key on the strict default
|
|
550
|
+
// rather than throwing mid-render.
|
|
551
|
+
var additions = {};
|
|
549
552
|
if (Array.isArray(keys)) {
|
|
550
553
|
keys.forEach(function (key) {
|
|
551
554
|
var hostSet = CSP_HOSTS[key];
|
|
552
555
|
if (!hostSet) return;
|
|
553
556
|
Object.keys(_SCOPED_DIRECTIVES).forEach(function (directive) {
|
|
554
|
-
var
|
|
555
|
-
var hosts = hostSet[subKey];
|
|
557
|
+
var hosts = hostSet[_SCOPED_DIRECTIVES[directive]];
|
|
556
558
|
if (!hosts || !hosts.length) return;
|
|
557
|
-
|
|
558
|
-
// The default omits frame-src (it relies on the default-src /
|
|
559
|
-
// fenced-frame-src pair); create it from 'self' so admitting a
|
|
560
|
-
// provider frame doesn't accidentally open it to everything.
|
|
561
|
-
map[directive] = ["'self'"];
|
|
562
|
-
order.push(directive);
|
|
563
|
-
}
|
|
564
|
-
hosts.forEach(function (h) {
|
|
565
|
-
if (map[directive].indexOf(h) === -1) map[directive].push(h);
|
|
566
|
-
});
|
|
559
|
+
additions[directive] = (additions[directive] || []).concat(hosts);
|
|
567
560
|
});
|
|
568
561
|
});
|
|
569
562
|
}
|
|
570
563
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
564
|
+
// b.csp.mergeDirectives owns the merge: append-and-dedupe per directive,
|
|
565
|
+
// seed a directive the base omits rather than leaving it wide open, and
|
|
566
|
+
// validate only what was added so the trusted default round-trips
|
|
567
|
+
// untouched. It replaces a CSP parser written here, which had to be kept
|
|
568
|
+
// in step with whatever shape the vendored default took.
|
|
569
|
+
//
|
|
570
|
+
// The primitive throws on a directive or source it will not accept. This
|
|
571
|
+
// runs per response while rendering a payment page, and the documented
|
|
572
|
+
// contract is that a bad input degrades to the strict default rather than
|
|
573
|
+
// failing the render — so a refusal is caught and answered with exactly
|
|
574
|
+
// that. Failing closed here means the provider is refused, never admitted
|
|
575
|
+
// more broadly than asked.
|
|
576
|
+
// The refusal is recorded rather than swallowed: it means a host in
|
|
577
|
+
// CSP_HOSTS is one the primitive will not accept, which is an operator
|
|
578
|
+
// configuration problem that would otherwise show up only as a payment SDK
|
|
579
|
+
// silently failing to load.
|
|
580
|
+
try {
|
|
581
|
+
return b.csp.mergeDirectives(undefined, additions);
|
|
582
|
+
} catch (e) {
|
|
583
|
+
b.audit.safeEmit({
|
|
584
|
+
action: "shop.security.scopedCsp.refused",
|
|
585
|
+
outcome: "failure",
|
|
586
|
+
metadata: {
|
|
587
|
+
keys: Array.isArray(keys) ? keys.join(",") : String(keys),
|
|
588
|
+
reason: (e && e.message) || "unknown",
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
return _vendoredSecurityHeaders.DEFAULT_CSP;
|
|
592
|
+
}
|
|
575
593
|
}
|
|
576
594
|
|
|
577
595
|
// ---- route-scoped Permissions-Policy (payment + passkey surfaces) --------
|
|
@@ -618,7 +636,10 @@ function scopedCsp(keys) {
|
|
|
618
636
|
// top-level same-origin document). Quote-wrapped per RFC 9651 structured
|
|
619
637
|
// fields. Single source — every page that enables the payment feature names
|
|
620
638
|
// it here.
|
|
621
|
-
|
|
639
|
+
// Allowlist VALUES, without the `feature=` prefix — that is the shape
|
|
640
|
+
// b.csp.mergePermissionsPolicy takes, and it keeps the feature name in exactly
|
|
641
|
+
// one place (the registry key) rather than repeated inside its own value.
|
|
642
|
+
var _PAYMENT_ALLOWLIST = '(self "https://js.stripe.com" "https://pay.google.com")';
|
|
622
643
|
|
|
623
644
|
// WebAuthn assertion / attestation run in the page's OWN top-level document
|
|
624
645
|
// (the islands call navigator.credentials.get / .create directly, never from
|
|
@@ -628,8 +649,8 @@ var _PAYMENT_ALLOWLIST = 'payment=(self "https://js.stripe.com" "https://pay.goo
|
|
|
628
649
|
// still cannot drive WebAuthn. Single source — every passkey page names the
|
|
629
650
|
// feature → allowlist mapping here.
|
|
630
651
|
var _PASSKEY_FEATURE_ALLOWLIST = {
|
|
631
|
-
"publickey-credentials-get": "
|
|
632
|
-
"publickey-credentials-create": "
|
|
652
|
+
"publickey-credentials-get": "(self)",
|
|
653
|
+
"publickey-credentials-create": "(self)",
|
|
633
654
|
};
|
|
634
655
|
|
|
635
656
|
// Feature name → its scoped allowlist token. The pay surface relaxes
|
|
@@ -670,12 +691,25 @@ function scopedPermissionsPolicy(opts) {
|
|
|
670
691
|
overrides[name] = _SCOPED_FEATURE_OVERRIDES[name];
|
|
671
692
|
}
|
|
672
693
|
});
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
694
|
+
|
|
695
|
+
// b.csp.mergePermissionsPolicy applies the overrides onto the vendored
|
|
696
|
+
// denylist, replacing a map-and-rejoin over that list here. The registry
|
|
697
|
+
// filter above still runs first, so an unnamed feature cannot be relaxed and
|
|
698
|
+
// an unknown one is dropped rather than passed to a primitive that refuses
|
|
699
|
+
// it — which preserves the documented never-throw behaviour on a render
|
|
700
|
+
// path. A refusal is still possible if a registry VALUE is malformed, and
|
|
701
|
+
// that is an operator configuration error worth surfacing rather than
|
|
702
|
+
// hiding behind a silently strict header.
|
|
703
|
+
try {
|
|
704
|
+
return b.csp.mergePermissionsPolicy(undefined, overrides);
|
|
705
|
+
} catch (e) {
|
|
706
|
+
b.audit.safeEmit({
|
|
707
|
+
action: "shop.security.scopedPermissionsPolicy.refused",
|
|
708
|
+
outcome: "failure",
|
|
709
|
+
metadata: { features: features.join(","), reason: (e && e.message) || "unknown" },
|
|
710
|
+
});
|
|
711
|
+
return _vendoredSecurityHeaders.DEFAULT_PERMISSIONS.join(", ");
|
|
712
|
+
}
|
|
679
713
|
}
|
|
680
714
|
|
|
681
715
|
/**
|
|
@@ -732,7 +766,7 @@ function botGuardOpts() {
|
|
|
732
766
|
// directory (e.g. /.well-known/apple-...-association/anything), re-opening
|
|
733
767
|
// the guard on routes that aren't the single static association file.
|
|
734
768
|
.concat(PUBLIC_WELL_KNOWN_PATHS.map(function (p) {
|
|
735
|
-
return new RegExp("^" +
|
|
769
|
+
return new RegExp("^" + b.codepointClass.escapeRegExp(p) + "$");
|
|
736
770
|
}))
|
|
737
771
|
.concat([/^\/admin(\/|$)/]),
|
|
738
772
|
};
|
|
@@ -745,6 +779,28 @@ function _hasPrefix(pathname, prefixes) {
|
|
|
745
779
|
return false;
|
|
746
780
|
}
|
|
747
781
|
|
|
782
|
+
// Skip matchers for the request-lifecycle guards, built once.
|
|
783
|
+
//
|
|
784
|
+
// b.requestHelpers.makeSkipMatcher takes the REQUEST rather than a pathname
|
|
785
|
+
// the caller derived, which removes three copies of `req.pathname || req.url
|
|
786
|
+
// || "/"` and keeps the exemption correct if a guard is ever mounted somewhere
|
|
787
|
+
// the router has not already set `req.pathname` — the matcher strips the query
|
|
788
|
+
// string itself rather than trusting the caller to have done it.
|
|
789
|
+
//
|
|
790
|
+
// Two matchers because the semantics differ: a webhook path is a PREFIX (the
|
|
791
|
+
// processor may append to it) while the health path is EXACT (`/_/healthz`
|
|
792
|
+
// must not inherit the exemption).
|
|
793
|
+
var _skipWebhookPath = b.requestHelpers.makeSkipMatcher(
|
|
794
|
+
{ skipPaths: WEBHOOK_PATHS.slice() }, "securityMiddleware.webhookSkip");
|
|
795
|
+
var _skipHealthPath = b.requestHelpers.makeSkipMatcher(
|
|
796
|
+
{ skipPaths: [HEALTH_PATH], exact: true }, "securityMiddleware.healthSkip");
|
|
797
|
+
|
|
798
|
+
// Both guards exempt the same set: a server-to-server webhook authenticated by
|
|
799
|
+
// its own signature, and the liveness probe.
|
|
800
|
+
function _isUnguardedPath(req) {
|
|
801
|
+
return _skipWebhookPath(req) || _skipHealthPath(req);
|
|
802
|
+
}
|
|
803
|
+
|
|
748
804
|
/**
|
|
749
805
|
* Mount the per-route tight rate limiters + the fetch-metadata gate on
|
|
750
806
|
* the router inside the operator's `routes(r)` chain. Call AFTER the
|
|
@@ -803,7 +859,7 @@ function mountRouteGuards(r) {
|
|
|
803
859
|
});
|
|
804
860
|
r.use(function csrfGuard(req, res, next) {
|
|
805
861
|
var pathname = req.pathname || req.url || "/";
|
|
806
|
-
if (
|
|
862
|
+
if (_isUnguardedPath(req)) return next();
|
|
807
863
|
if (isEdgeExemptPath(pathname)) return next();
|
|
808
864
|
return csrfGate(req, res, next);
|
|
809
865
|
});
|
|
@@ -825,8 +881,7 @@ function mountRouteGuards(r) {
|
|
|
825
881
|
allowedNavigate: true,
|
|
826
882
|
});
|
|
827
883
|
r.use(function fetchMetadataGuard(req, res, next) {
|
|
828
|
-
|
|
829
|
-
if (_hasPrefix(pathname, WEBHOOK_PATHS)) return next();
|
|
884
|
+
if (_skipWebhookPath(req)) return next();
|
|
830
885
|
return fmGate(req, res, next);
|
|
831
886
|
});
|
|
832
887
|
|
|
@@ -880,7 +935,7 @@ function mountRouteGuards(r) {
|
|
|
880
935
|
r.use(function tightRateGuard(req, res, next) {
|
|
881
936
|
var pathname = req.pathname || req.url || "/";
|
|
882
937
|
// Never throttle the webhook or health paths.
|
|
883
|
-
if (
|
|
938
|
+
if (_isUnguardedPath(req)) return next();
|
|
884
939
|
if (!_hasPrefix(pathname, TIGHT_PREFIXES)) return next();
|
|
885
940
|
// `/products/`, `/gift-cards`, and `/orders/` carry GET reads (PDP,
|
|
886
941
|
// balance page render, order confirmation page) that a shopper hits
|