@blamejs/blamejs-shop 0.5.20 → 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 +2 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +14 -5
- 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 +47 -23
- 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/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/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 +86 -58
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +5 -16
- 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 +2 -2
- 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-invoices.js +3 -6
- package/lib/vendors.js +5 -19
- package/lib/webhook-receiver.js +3 -10
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- package/package.json +1 -1
package/lib/support-tickets.js
CHANGED
|
@@ -62,6 +62,7 @@ var DEFAULT_LIST_LIMIT = 25;
|
|
|
62
62
|
var EMAIL_NAMESPACE = "support-ticket-email";
|
|
63
63
|
|
|
64
64
|
var b = require("./vendor/blamejs");
|
|
65
|
+
var textGuard = require("./text-guard");
|
|
65
66
|
|
|
66
67
|
var ALLOWED_AUTHORS = ["customer", "operator", "system"];
|
|
67
68
|
var ALLOWED_CATEGORIES = [
|
|
@@ -104,14 +105,9 @@ var LIST_ORDER_KEY = ["opened_at:desc", "id:desc"];
|
|
|
104
105
|
// Refuse C0 control bytes + DEL. Newlines (LF/CR) + tab are tolerated
|
|
105
106
|
// in body so multi-line customer narratives work; subject is single-
|
|
106
107
|
// line by convention and refuses them.
|
|
107
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
108
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
109
108
|
// Zero-width + direction-override family (see orderNotes for the
|
|
110
109
|
// same justification). Spelled via \u escapes so the file stays
|
|
111
110
|
// ESLint no-irregular-whitespace clean.
|
|
112
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
113
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
114
|
-
);
|
|
115
111
|
|
|
116
112
|
// ---- validators ---------------------------------------------------------
|
|
117
113
|
|
|
@@ -159,12 +155,7 @@ function _subject(s) {
|
|
|
159
155
|
if (s.length > MAX_SUBJECT_LEN) {
|
|
160
156
|
throw new TypeError("supportTickets: subject must be <= " + MAX_SUBJECT_LEN + " characters");
|
|
161
157
|
}
|
|
162
|
-
|
|
163
|
-
throw new TypeError("supportTickets: subject contains control bytes");
|
|
164
|
-
}
|
|
165
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
166
|
-
throw new TypeError("supportTickets: subject contains zero-width / direction-override characters");
|
|
167
|
-
}
|
|
158
|
+
textGuard.freeText(s, "supportTickets: subject", { singleLine: "reject", zeroWidth: "reject" });
|
|
168
159
|
return s;
|
|
169
160
|
}
|
|
170
161
|
|
|
@@ -179,12 +170,7 @@ function _body(s) {
|
|
|
179
170
|
if (s.length > MAX_BODY_LEN) {
|
|
180
171
|
throw new TypeError("supportTickets: body must be <= " + MAX_BODY_LEN + " characters");
|
|
181
172
|
}
|
|
182
|
-
|
|
183
|
-
throw new TypeError("supportTickets: body contains control bytes");
|
|
184
|
-
}
|
|
185
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
186
|
-
throw new TypeError("supportTickets: body contains zero-width / direction-override characters");
|
|
187
|
-
}
|
|
173
|
+
textGuard.freeText(s, "supportTickets: body", { zeroWidth: "reject" });
|
|
188
174
|
return s;
|
|
189
175
|
}
|
|
190
176
|
|
|
@@ -205,7 +191,7 @@ function _tags(input) {
|
|
|
205
191
|
if (t.length > MAX_TAG_LEN) {
|
|
206
192
|
throw new TypeError("supportTickets: tags[" + i + "] must be <= " + MAX_TAG_LEN + " characters");
|
|
207
193
|
}
|
|
208
|
-
if (
|
|
194
|
+
if (textGuard.hasCodepointThreat(t, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
209
195
|
throw new TypeError("supportTickets: tags[" + i + "] contains control / zero-width bytes");
|
|
210
196
|
}
|
|
211
197
|
out.push(t);
|
|
@@ -220,9 +206,7 @@ function _singleTag(t) {
|
|
|
220
206
|
if (t.length > MAX_TAG_LEN) {
|
|
221
207
|
throw new TypeError("supportTickets: tag must be <= " + MAX_TAG_LEN + " characters");
|
|
222
208
|
}
|
|
223
|
-
|
|
224
|
-
throw new TypeError("supportTickets: tag contains control / zero-width bytes");
|
|
225
|
-
}
|
|
209
|
+
textGuard.freeText(t, "supportTickets: tag", { singleLine: "reject", zeroWidth: "reject" });
|
|
226
210
|
return t;
|
|
227
211
|
}
|
|
228
212
|
|
|
@@ -235,9 +219,7 @@ function _reason(r) {
|
|
|
235
219
|
if (r.length > MAX_REASON_LEN) {
|
|
236
220
|
throw new TypeError("supportTickets: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
237
221
|
}
|
|
238
|
-
|
|
239
|
-
throw new TypeError("supportTickets: reason contains control / zero-width bytes");
|
|
240
|
-
}
|
|
222
|
+
textGuard.freeText(r, "supportTickets: reason", { zeroWidth: "reject" });
|
|
241
223
|
return r;
|
|
242
224
|
}
|
|
243
225
|
|
package/lib/tax-remittance.js
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
88
|
var b = require("./vendor/blamejs");
|
|
89
|
+
var textGuard = require("./text-guard");
|
|
89
90
|
|
|
90
91
|
var C = b.constants;
|
|
91
92
|
|
|
@@ -107,7 +108,6 @@ var DEFAULT_DAYS_LATE_MIN = 1;
|
|
|
107
108
|
var MAX_DAYS_LATE_MIN = 3650; // ~10 years
|
|
108
109
|
var MS_PER_DAY = C.TIME.days(1);
|
|
109
110
|
|
|
110
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
111
111
|
|
|
112
112
|
// ---- monotonic clock ----------------------------------------------------
|
|
113
113
|
//
|
|
@@ -182,7 +182,7 @@ function _shortText(s, label, max) {
|
|
|
182
182
|
throw new TypeError("taxRemittance: " + label +
|
|
183
183
|
" must be a non-empty string <= " + max + " chars");
|
|
184
184
|
}
|
|
185
|
-
if (
|
|
185
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
186
186
|
throw new TypeError("taxRemittance: " + label + " must not contain control bytes");
|
|
187
187
|
}
|
|
188
188
|
return s;
|
package/lib/tenants.js
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
58
|
var b = require("./vendor/blamejs");
|
|
59
|
+
var textGuard = require("./text-guard");
|
|
59
60
|
|
|
60
61
|
var MAX_SLUG_LEN = 64;
|
|
61
62
|
var MAX_NAME_LEN = 200;
|
|
@@ -95,10 +96,6 @@ var THEME_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
|
95
96
|
// renders in operator dashboards; embedded control / direction-
|
|
96
97
|
// override bytes are a slipping-class for visual-spoofing attacks
|
|
97
98
|
// downstream.
|
|
98
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
99
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
100
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
101
|
-
);
|
|
102
99
|
|
|
103
100
|
var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
104
101
|
"name", "default_currency", "default_locale", "theme_slug",
|
|
@@ -131,9 +128,7 @@ function _name(s) {
|
|
|
131
128
|
if (s.length > MAX_NAME_LEN) {
|
|
132
129
|
throw new TypeError("tenants: name must be <= " + MAX_NAME_LEN + " characters");
|
|
133
130
|
}
|
|
134
|
-
|
|
135
|
-
throw new TypeError("tenants: name contains control / zero-width bytes");
|
|
136
|
-
}
|
|
131
|
+
textGuard.freeText(s, "tenants: name", { singleLine: "reject", zeroWidth: "reject" });
|
|
137
132
|
return s;
|
|
138
133
|
}
|
|
139
134
|
|
package/lib/text-guard.js
CHANGED
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
* - slugLabel(s, label, opts?) — lowercase URL-slug label
|
|
37
37
|
* - hostLabel(host, label?) — outbound host SSRF + by-name denylist
|
|
38
38
|
* - freeText(s, label, policy?) — dangerous-codepoint screen for
|
|
39
|
-
* unconstrained UTF-8 text fields
|
|
39
|
+
* unconstrained UTF-8 text fields, throwing on a refusal
|
|
40
|
+
* - hasCodepointThreat(s, policy?) — the same screen for a defensive
|
|
41
|
+
* reader that DROPS a bad value instead of refusing the request
|
|
42
|
+
* - firstCodepointThreat(s, policy?) — the one decision both run, so
|
|
43
|
+
* the throwing and dropping paths cannot disagree
|
|
40
44
|
*
|
|
41
45
|
* @primitive textGuard
|
|
42
46
|
* @related b.money, b.ssrfGuard, b.safeUrl, shop.currencyRounding,
|
|
@@ -52,6 +56,41 @@ var b = require("./vendor/blamejs");
|
|
|
52
56
|
// second copy of the tables.
|
|
53
57
|
var codepointClass = b.codepointClass;
|
|
54
58
|
|
|
59
|
+
// Two invisible-codepoint groups this shop refuses that the framework catalog
|
|
60
|
+
// does not carry yet. Built with codepointClass.charClass off a range table so
|
|
61
|
+
// the escaping is the catalog's, not a second hand-spelled class:
|
|
62
|
+
//
|
|
63
|
+
// U+007F DELETE — grouped with the C0 controls everywhere it matters, but
|
|
64
|
+
// outside the 0x00-0x1F block the framework's C0_CTRL_RANGES covers.
|
|
65
|
+
// U+2061-2064 FUNCTION APPLICATION / INVISIBLE TIMES / INVISIBLE SEPARATOR /
|
|
66
|
+
// INVISIBLE PLUS — zero-width invisible operators, the same spoofing class
|
|
67
|
+
// as U+2060 WORD JOINER, which the framework's ZERO_WIDTH_RANGES does have.
|
|
68
|
+
//
|
|
69
|
+
// Requested upstream (blamejs#580). When a vendor refresh lands them in the
|
|
70
|
+
// catalog this supplement becomes dead weight and comes out — the drift test
|
|
71
|
+
// in test/layer-1-state/text-guard.test.js fails the moment that happens, so
|
|
72
|
+
// it cannot be silently carried forever.
|
|
73
|
+
//
|
|
74
|
+
// Kept as two classes rather than one so each joins the check whose refusal
|
|
75
|
+
// message is already true of it.
|
|
76
|
+
// allow:dynamic-regex — codepoints from a literal range table
|
|
77
|
+
var DEL_RE = new RegExp("[" + codepointClass.charClass([0x007F]) + "]");
|
|
78
|
+
// allow:dynamic-regex — codepoints from a literal range table
|
|
79
|
+
var INVISIBLE_OP_RE = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064]]) + "]");
|
|
80
|
+
// Global twin for the strip pass; the unglobbed one above is the presence test.
|
|
81
|
+
// allow:dynamic-regex — codepoints from a literal range table
|
|
82
|
+
var INVISIBLE_OP_RE_G = new RegExp("[" + codepointClass.charClass([[0x2061, 0x2064]]) + "]", "g");
|
|
83
|
+
|
|
84
|
+
// Everything that ends a line or pads it out, for a value that must stay on
|
|
85
|
+
// one. Tab / LF / CR sit outside the framework's C0 table on purpose — a
|
|
86
|
+
// multi-line body needs them — so a single-line field asks for them here.
|
|
87
|
+
// U+2028 / U+2029 are not control characters at all, but they terminate a
|
|
88
|
+
// line for both a renderer and a JS string literal.
|
|
89
|
+
// allow:dynamic-regex — codepoints from a literal range table
|
|
90
|
+
var LINE_BREAK_RE = new RegExp(
|
|
91
|
+
"[" + codepointClass.charClass([0x0009, 0x000A, 0x000D, [0x2028, 0x2029]]) + "]"
|
|
92
|
+
);
|
|
93
|
+
|
|
55
94
|
// ---- shop validators ----------------------------------------------------
|
|
56
95
|
|
|
57
96
|
// asciiUpperLetters(s, label, n?) — exactly `n` (default 3) ASCII
|
|
@@ -157,45 +196,105 @@ function hostLabel(host, label) {
|
|
|
157
196
|
return h;
|
|
158
197
|
}
|
|
159
198
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
// firstCodepointThreat(s, policy?) — the single decision both the throwing
|
|
200
|
+
// screen (freeText) and the drop-silent one (hasCodepointThreat) run, so a
|
|
201
|
+
// field guarded one way and a field guarded the other can never disagree
|
|
202
|
+
// about what "dangerous" means. Returns a short reason string naming the
|
|
203
|
+
// class, or null when the text is clean. The reason is the clause that
|
|
204
|
+
// follows the field name in a refusal ("contains a null byte"), so a caller
|
|
205
|
+
// building a message just joins the two.
|
|
206
|
+
//
|
|
207
|
+
// Policies, each "reject" to enable and any other value to allow:
|
|
208
|
+
// bidi — default "reject". Bidi overrides (CVE-2021-42574).
|
|
209
|
+
// nullByte — default "reject". U+0000.
|
|
210
|
+
// control — default "reject". C0 controls + U+007F DELETE. Tab, LF and
|
|
211
|
+
// CR are ALLOWED here: a multi-line body legitimately has
|
|
212
|
+
// them. Use singleLine for a field that must not.
|
|
213
|
+
// singleLine — off by default. Tab / LF / CR, plus U+2028 LINE SEPARATOR
|
|
214
|
+
// and U+2029 PARAGRAPH SEPARATOR, which break a line without
|
|
215
|
+
// being C0 at all. For a value rendered on one line — a URL,
|
|
216
|
+
// a subject, a title, a header value.
|
|
217
|
+
// zeroWidth — off by default. Zero-width / invisible formatting. Includes
|
|
218
|
+
// U+00AD SOFT HYPHEN, which text pasted out of a hyphenating
|
|
219
|
+
// word processor or PDF carries invisibly.
|
|
220
|
+
// mixedScript — off by default. Confusable / homograph across writing
|
|
221
|
+
// systems, narrowed by policy.allowedScripts.
|
|
222
|
+
function firstCodepointThreat(s, policy) {
|
|
175
223
|
policy = policy || {};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
var controlP = policy.control || "reject";
|
|
179
|
-
if (bidiP === "reject" && codepointClass.BIDI_RE.test(s)) {
|
|
180
|
-
throw new TypeError(name + " contains a Unicode bidi override (CVE-2021-42574 Trojan Source)");
|
|
224
|
+
if ((policy.bidi || "reject") === "reject" && codepointClass.BIDI_RE.test(s)) {
|
|
225
|
+
return "contains a Unicode bidi override (CVE-2021-42574 Trojan Source)";
|
|
181
226
|
}
|
|
182
|
-
if (
|
|
183
|
-
|
|
227
|
+
if ((policy.nullByte || "reject") === "reject" &&
|
|
228
|
+
s.indexOf(codepointClass.NULL_BYTE) !== -1) {
|
|
229
|
+
return "contains a null byte";
|
|
184
230
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
231
|
+
if ((policy.control || "reject") === "reject" &&
|
|
232
|
+
(codepointClass.C0_CTRL_RE.test(s) || DEL_RE.test(s))) {
|
|
233
|
+
return "contains a C0 control character";
|
|
187
234
|
}
|
|
188
|
-
if (policy.
|
|
189
|
-
|
|
235
|
+
if (policy.singleLine === "reject" && LINE_BREAK_RE.test(s)) {
|
|
236
|
+
return "contains a line break or tab but must be a single line";
|
|
237
|
+
}
|
|
238
|
+
if (policy.zeroWidth === "reject" &&
|
|
239
|
+
(codepointClass.ZERO_WIDTH_RE.test(s) || INVISIBLE_OP_RE.test(s))) {
|
|
240
|
+
return "contains a zero-width / invisible formatting character";
|
|
190
241
|
}
|
|
191
242
|
if (policy.mixedScript === "reject") {
|
|
192
243
|
var scripts = codepointClass.detectMixedScripts(s, policy.allowedScripts);
|
|
193
244
|
if (scripts) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
245
|
+
return "mixes writing systems (" + scripts.join(", ") +
|
|
246
|
+
") — possible confusable / homograph";
|
|
197
247
|
}
|
|
198
248
|
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// hasCodepointThreat(s, policy?) — the non-throwing sibling of freeText, for
|
|
253
|
+
// the defensive readers that DROP a bad value rather than refusing the
|
|
254
|
+
// request (a link URL inside a rendered body, a header line being assembled).
|
|
255
|
+
// A non-string is dangerous by definition here: the caller is about to render
|
|
256
|
+
// or transmit it. Returns true when the text must not be used.
|
|
257
|
+
function hasCodepointThreat(s, policy) {
|
|
258
|
+
if (typeof s !== "string") return true;
|
|
259
|
+
return firstCodepointThreat(s, policy) !== null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// scrubInvisible(s) — remove every bidi override and invisible-formatting
|
|
263
|
+
// codepoint and return the cleaned string. The third disposition, alongside
|
|
264
|
+
// throwing and dropping: for a value that must be ACCEPTED whatever it
|
|
265
|
+
// contains, because refusing it would hand an attacker a denial-of-service
|
|
266
|
+
// against a legitimate user (a search query is the standing example — a
|
|
267
|
+
// pasted BOM must not cost the shopper their search).
|
|
268
|
+
//
|
|
269
|
+
// Composes the framework's strip pass and then removes the codepoints the
|
|
270
|
+
// catalog does not carry yet, so what this deletes is exactly what
|
|
271
|
+
// hasCodepointThreat would have objected to under zeroWidth: "reject".
|
|
272
|
+
// Control characters are deliberately NOT touched: a caller stripping those
|
|
273
|
+
// usually wants to substitute a space to avoid welding two words together,
|
|
274
|
+
// which is its own decision.
|
|
275
|
+
function scrubInvisible(s) {
|
|
276
|
+
if (typeof s !== "string") return s;
|
|
277
|
+
var out = codepointClass.applyCharStripPolicies(s, {
|
|
278
|
+
bidiPolicy: "strip",
|
|
279
|
+
zeroWidthPolicy: "strip",
|
|
280
|
+
tagsPolicy: "strip",
|
|
281
|
+
});
|
|
282
|
+
return out.replace(INVISIBLE_OP_RE_G, "");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// freeText(s, label, policy?) — the dangerous-codepoint screen for an
|
|
286
|
+
// UNCONSTRAINED UTF-8 text field (customer note, review body, gift
|
|
287
|
+
// message, product Q&A). Unlike the ASCII-allowlist validators above,
|
|
288
|
+
// these fields legitimately carry arbitrary letters, so the codepoint
|
|
289
|
+
// catalog is the right backing. Throws TypeError on a refused codepoint;
|
|
290
|
+
// returns the input on success. See firstCodepointThreat for the policies.
|
|
291
|
+
function freeText(s, label, policy) {
|
|
292
|
+
var name = label || "value";
|
|
293
|
+
if (typeof s !== "string") {
|
|
294
|
+
throw new TypeError(name + " must be a string");
|
|
295
|
+
}
|
|
296
|
+
var reason = firstCodepointThreat(s, policy);
|
|
297
|
+
if (reason) throw new TypeError(name + " " + reason);
|
|
199
298
|
return s;
|
|
200
299
|
}
|
|
201
300
|
|
|
@@ -213,9 +312,12 @@ module.exports = {
|
|
|
213
312
|
applyCharStripPolicies: codepointClass.applyCharStripPolicies,
|
|
214
313
|
|
|
215
314
|
// Shop validators.
|
|
216
|
-
asciiUpperLetters:
|
|
217
|
-
currencyCode:
|
|
218
|
-
slugLabel:
|
|
219
|
-
hostLabel:
|
|
220
|
-
freeText:
|
|
315
|
+
asciiUpperLetters: asciiUpperLetters,
|
|
316
|
+
currencyCode: currencyCode,
|
|
317
|
+
slugLabel: slugLabel,
|
|
318
|
+
hostLabel: hostLabel,
|
|
319
|
+
freeText: freeText,
|
|
320
|
+
hasCodepointThreat: hasCodepointThreat,
|
|
321
|
+
firstCodepointThreat: firstCodepointThreat,
|
|
322
|
+
scrubInvisible: scrubInvisible,
|
|
221
323
|
};
|
package/lib/theme-assets.js
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
*/
|
|
85
85
|
|
|
86
86
|
var b = require("./vendor/blamejs");
|
|
87
|
+
var textGuard = require("./text-guard");
|
|
87
88
|
|
|
88
89
|
var MAX_SLUG_LEN = 80;
|
|
89
90
|
var MAX_THEME_SLUG_LEN = 80;
|
|
@@ -130,14 +131,10 @@ var SHA3_512_HEX_RE = /^[0-9a-f]{128}$/;
|
|
|
130
131
|
var CONTENT_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:\s*;\s*[A-Za-z0-9!#$&^_.+-]+\s*=\s*(?:"[^"\x00-\x1f]*"|[A-Za-z0-9!#$&^_.+-]+))*$/;
|
|
131
132
|
|
|
132
133
|
// Refuse C0 control bytes + DEL in operator-authored text fields.
|
|
133
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
134
134
|
|
|
135
135
|
// Zero-width / direction-override family — mirrors the rest of the
|
|
136
136
|
// shop primitives. Spelled with \u-escapes so ESLint's
|
|
137
137
|
// no-irregular-whitespace stays happy.
|
|
138
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
139
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
140
|
-
);
|
|
141
138
|
|
|
142
139
|
var MS_PER_DAY = b.constants.TIME.days(1);
|
|
143
140
|
|
|
@@ -181,9 +178,7 @@ function _contentType(s) {
|
|
|
181
178
|
MAX_CONTENT_TYPE_LEN + " chars"
|
|
182
179
|
);
|
|
183
180
|
}
|
|
184
|
-
|
|
185
|
-
throw new TypeError("themeAssets: content_type contains control bytes");
|
|
186
|
-
}
|
|
181
|
+
textGuard.freeText(s, "themeAssets: content_type", { singleLine: "reject" });
|
|
187
182
|
if (!CONTENT_TYPE_RE.test(s)) {
|
|
188
183
|
throw new TypeError(
|
|
189
184
|
"themeAssets: content_type " + JSON.stringify(s) +
|
|
@@ -230,9 +225,7 @@ function _sourceUrl(s) {
|
|
|
230
225
|
MAX_SOURCE_URL_LEN + " chars"
|
|
231
226
|
);
|
|
232
227
|
}
|
|
233
|
-
|
|
234
|
-
throw new TypeError("themeAssets: source_url contains control / zero-width bytes");
|
|
235
|
-
}
|
|
228
|
+
textGuard.freeText(s, "themeAssets: source_url", { singleLine: "reject", zeroWidth: "reject" });
|
|
236
229
|
if (s.charCodeAt(0) === 47 /* "/" */) {
|
|
237
230
|
if (s.length > 1 && s.charCodeAt(1) === 47) {
|
|
238
231
|
throw new TypeError(
|
|
@@ -264,12 +257,7 @@ function _altText(s) {
|
|
|
264
257
|
MAX_ALT_TEXT_LEN + " chars (or null)"
|
|
265
258
|
);
|
|
266
259
|
}
|
|
267
|
-
|
|
268
|
-
throw new TypeError("themeAssets: alt_text contains control bytes (incl. CR/LF)");
|
|
269
|
-
}
|
|
270
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
271
|
-
throw new TypeError("themeAssets: alt_text contains zero-width / direction-override characters");
|
|
272
|
-
}
|
|
260
|
+
textGuard.freeText(s, "themeAssets: alt_text", { singleLine: "reject", zeroWidth: "reject" });
|
|
273
261
|
return s;
|
|
274
262
|
}
|
|
275
263
|
|
package/lib/translations.js
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
60
|
var b = require("./vendor/blamejs");
|
|
61
|
+
var textGuard = require("./text-guard");
|
|
61
62
|
|
|
62
63
|
var MAX_KIND_LEN = 64;
|
|
63
64
|
var MAX_RESOURCE_ID_LEN = 128;
|
|
@@ -90,14 +91,10 @@ var LOCALE_RE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
|
90
91
|
// allowed (operators author multi-line page bodies) — the rest of
|
|
91
92
|
// the C0 range would only reach the storefront as a smuggling
|
|
92
93
|
// payload or a copy-paste accident from a terminal.
|
|
93
|
-
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
94
94
|
|
|
95
95
|
// Zero-width / direction-override family. Spelled with \u-escapes
|
|
96
96
|
// so ESLint's no-irregular-whitespace stays happy. Same defence the
|
|
97
97
|
// rest of the shop uses on operator-authored strings.
|
|
98
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
99
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
100
|
-
);
|
|
101
98
|
|
|
102
99
|
// ---- validators ---------------------------------------------------------
|
|
103
100
|
|
|
@@ -193,10 +190,10 @@ function _value(s, fnLabel) {
|
|
|
193
190
|
if (s.length > MAX_VALUE_LEN) {
|
|
194
191
|
throw new TypeError(fnLabel + ": value must be <= " + MAX_VALUE_LEN + " characters");
|
|
195
192
|
}
|
|
196
|
-
if (
|
|
193
|
+
if (textGuard.hasCodepointThreat(s)) {
|
|
197
194
|
throw new TypeError(fnLabel + ": value contains forbidden control bytes");
|
|
198
195
|
}
|
|
199
|
-
if (
|
|
196
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
200
197
|
throw new TypeError(fnLabel + ": value contains zero-width / direction-override characters");
|
|
201
198
|
}
|
|
202
199
|
return s;
|
package/lib/trust-badges.js
CHANGED
|
@@ -116,13 +116,9 @@ var EVENT_TYPES = Object.freeze(["impression", "click"]);
|
|
|
116
116
|
var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
117
117
|
|
|
118
118
|
// Refuse C0 control bytes + DEL in single-line operator strings.
|
|
119
|
-
var CONTROL_BYTE_LINE_RE = /[\x00-\x1f\x7f]/;
|
|
120
119
|
|
|
121
120
|
// Zero-width / direction-override family — same catalogue as the
|
|
122
121
|
// other operator-authored primitives.
|
|
123
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
124
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
125
|
-
);
|
|
126
122
|
|
|
127
123
|
// Hash namespace — opaque per-primitive constant so a session_id_hash
|
|
128
124
|
// emitted by trustBadges can't collide with one emitted by analytics
|
|
@@ -142,6 +138,7 @@ var ALLOWED_PATCH_COLUMNS = Object.freeze([
|
|
|
142
138
|
]);
|
|
143
139
|
|
|
144
140
|
var b = require("./vendor/blamejs");
|
|
141
|
+
var textGuard = require("./text-guard");
|
|
145
142
|
|
|
146
143
|
// ---- validators ---------------------------------------------------------
|
|
147
144
|
|
|
@@ -184,10 +181,10 @@ function _line(s, label, maxLen) {
|
|
|
184
181
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
185
182
|
throw new TypeError("trustBadges: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
186
183
|
}
|
|
187
|
-
if (
|
|
184
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
188
185
|
throw new TypeError("trustBadges: " + label + " contains control bytes (incl. CR/LF)");
|
|
189
186
|
}
|
|
190
|
-
if (
|
|
187
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
191
188
|
throw new TypeError("trustBadges: " + label + " contains zero-width / direction-override characters");
|
|
192
189
|
}
|
|
193
190
|
return s;
|
|
@@ -201,7 +198,7 @@ function _httpsOrRootUrl(s, label, maxLen) {
|
|
|
201
198
|
if (typeof s !== "string" || !s.length || s.length > maxLen) {
|
|
202
199
|
throw new TypeError("trustBadges: " + label + " must be a non-empty string ≤ " + maxLen + " chars");
|
|
203
200
|
}
|
|
204
|
-
if (
|
|
201
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject", zeroWidth: "reject" })) {
|
|
205
202
|
throw new TypeError("trustBadges: " + label + " contains control / zero-width bytes");
|
|
206
203
|
}
|
|
207
204
|
if (s.charCodeAt(0) === 47 /* "/" */) {
|
package/lib/vendor-invoices.js
CHANGED
|
@@ -95,10 +95,6 @@ var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
|
95
95
|
// as the sibling accounts-payable primitives — operator-rendered text
|
|
96
96
|
// fields refuse these to keep the dashboard + printout safe from
|
|
97
97
|
// header-injection + visual-spoofing attacks.
|
|
98
|
-
var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
99
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
100
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
101
|
-
);
|
|
102
98
|
|
|
103
99
|
var INVOICE_STATUSES = Object.freeze([
|
|
104
100
|
"received", "approved", "paid", "disputed", "voided",
|
|
@@ -107,6 +103,7 @@ var INVOICE_STATUSES = Object.freeze([
|
|
|
107
103
|
var MAX_DUE_WITHIN_DAYS = 3650; // ~10 years — bound the predicate
|
|
108
104
|
|
|
109
105
|
var b = require("./vendor/blamejs");
|
|
106
|
+
var textGuard = require("./text-guard");
|
|
110
107
|
|
|
111
108
|
var MS_PER_DAY = b.constants.TIME.days(1);
|
|
112
109
|
|
|
@@ -219,10 +216,10 @@ function _shortText(s, label, max) {
|
|
|
219
216
|
if (s.length > max) {
|
|
220
217
|
throw new TypeError("vendor-invoices: " + label + " must be <= " + max + " characters");
|
|
221
218
|
}
|
|
222
|
-
if (
|
|
219
|
+
if (textGuard.hasCodepointThreat(s, { singleLine: "reject" })) {
|
|
223
220
|
throw new TypeError("vendor-invoices: " + label + " contains control bytes");
|
|
224
221
|
}
|
|
225
|
-
if (
|
|
222
|
+
if (textGuard.hasCodepointThreat(s, { zeroWidth: "reject" })) {
|
|
226
223
|
throw new TypeError("vendor-invoices: " + label + " contains zero-width / direction-override bytes");
|
|
227
224
|
}
|
|
228
225
|
return s;
|
package/lib/vendors.js
CHANGED
|
@@ -97,10 +97,6 @@ var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
|
|
|
97
97
|
// direction-override bytes are a slipping-class for header injection
|
|
98
98
|
// + visual-spoofing attacks downstream. Spelled with \u-escapes so
|
|
99
99
|
// ESLint's no-irregular-whitespace stays happy.
|
|
100
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
101
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
102
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
103
|
-
);
|
|
104
100
|
|
|
105
101
|
// Mutable columns for `updateVendor(slug, patch)`. Slug is immutable
|
|
106
102
|
// — it's the public handle + the join-table key; mutating it would
|
|
@@ -119,6 +115,7 @@ var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
|
119
115
|
|
|
120
116
|
// Framework handle (the vendored blamejs); index.js re-exports this as .framework.
|
|
121
117
|
var b = require("./vendor/blamejs");
|
|
118
|
+
var textGuard = require("./text-guard");
|
|
122
119
|
|
|
123
120
|
// ---- validators ---------------------------------------------------------
|
|
124
121
|
|
|
@@ -159,12 +156,7 @@ function _name(s) {
|
|
|
159
156
|
if (s.length > MAX_NAME_LEN) {
|
|
160
157
|
throw new TypeError("vendors: name must be <= " + MAX_NAME_LEN + " characters");
|
|
161
158
|
}
|
|
162
|
-
|
|
163
|
-
throw new TypeError("vendors: name contains control bytes");
|
|
164
|
-
}
|
|
165
|
-
if (ZERO_WIDTH_RE.test(s)) {
|
|
166
|
-
throw new TypeError("vendors: name contains zero-width / direction-override bytes");
|
|
167
|
-
}
|
|
159
|
+
textGuard.freeText(s, "vendors: name", { singleLine: "reject", zeroWidth: "reject" });
|
|
168
160
|
return s;
|
|
169
161
|
}
|
|
170
162
|
|
|
@@ -186,9 +178,7 @@ function _payoutAddress(s) {
|
|
|
186
178
|
if (s.length > MAX_PAYOUT_ADDRESS_LEN) {
|
|
187
179
|
throw new TypeError("vendors: payout_address must be <= " + MAX_PAYOUT_ADDRESS_LEN + " characters");
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
throw new TypeError("vendors: payout_address contains control / zero-width bytes");
|
|
191
|
-
}
|
|
181
|
+
textGuard.freeText(s, "vendors: payout_address", { singleLine: "reject", zeroWidth: "reject" });
|
|
192
182
|
return s;
|
|
193
183
|
}
|
|
194
184
|
|
|
@@ -221,9 +211,7 @@ function _addressJson(value) {
|
|
|
221
211
|
if (encoded.length > MAX_ADDRESS_JSON_LEN) {
|
|
222
212
|
throw new TypeError("vendors: address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
|
|
223
213
|
}
|
|
224
|
-
|
|
225
|
-
throw new TypeError("vendors: address contains control / zero-width bytes");
|
|
226
|
-
}
|
|
214
|
+
textGuard.freeText(encoded, "vendors: address", { singleLine: "reject", zeroWidth: "reject" });
|
|
227
215
|
return encoded;
|
|
228
216
|
}
|
|
229
217
|
|
|
@@ -265,9 +253,7 @@ function _orderId(s) {
|
|
|
265
253
|
if (s.length > 256) {
|
|
266
254
|
throw new TypeError("vendors: order_id must be <= 256 characters");
|
|
267
255
|
}
|
|
268
|
-
|
|
269
|
-
throw new TypeError("vendors: order_id contains control / zero-width bytes");
|
|
270
|
-
}
|
|
256
|
+
textGuard.freeText(s, "vendors: order_id", { singleLine: "reject", zeroWidth: "reject" });
|
|
271
257
|
return s;
|
|
272
258
|
}
|
|
273
259
|
|
package/lib/webhook-receiver.js
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
*/
|
|
80
80
|
|
|
81
81
|
var b = require("./vendor/blamejs");
|
|
82
|
+
var textGuard = require("./text-guard");
|
|
82
83
|
var C = b.constants;
|
|
83
84
|
|
|
84
85
|
var SECRET_NAMESPACE = "webhook-receiver-secret";
|
|
@@ -118,10 +119,6 @@ var MAX_REASON_LEN = 1024;
|
|
|
118
119
|
|
|
119
120
|
var SIGNATURE_HEX_RE = /^[0-9a-f]{64}$/i; // 32 bytes of HMAC-SHA-256 = 64 hex chars
|
|
120
121
|
|
|
121
|
-
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
122
|
-
var ZERO_WIDTH_RE = new RegExp(
|
|
123
|
-
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
124
|
-
);
|
|
125
122
|
|
|
126
123
|
var STATUSES = ["received", "processed", "failed"];
|
|
127
124
|
|
|
@@ -198,9 +195,7 @@ function _outcome(s) {
|
|
|
198
195
|
if (s.length > MAX_OUTCOME_LEN) {
|
|
199
196
|
throw new TypeError("webhookReceiver: outcome must be <= " + MAX_OUTCOME_LEN + " characters");
|
|
200
197
|
}
|
|
201
|
-
|
|
202
|
-
throw new TypeError("webhookReceiver: outcome contains control / zero-width bytes");
|
|
203
|
-
}
|
|
198
|
+
textGuard.freeText(s, "webhookReceiver: outcome", { singleLine: "reject", zeroWidth: "reject" });
|
|
204
199
|
return s;
|
|
205
200
|
}
|
|
206
201
|
|
|
@@ -215,9 +210,7 @@ function _reason(s) {
|
|
|
215
210
|
if (s.length > MAX_REASON_LEN) {
|
|
216
211
|
throw new TypeError("webhookReceiver: reason must be <= " + MAX_REASON_LEN + " characters");
|
|
217
212
|
}
|
|
218
|
-
|
|
219
|
-
throw new TypeError("webhookReceiver: reason contains control / zero-width bytes");
|
|
220
|
-
}
|
|
213
|
+
textGuard.freeText(s, "webhookReceiver: reason", { singleLine: "reject", zeroWidth: "reject" });
|
|
221
214
|
return s;
|
|
222
215
|
}
|
|
223
216
|
|