@blamejs/core 0.18.49 → 0.18.51

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/lib/mail-sieve.js CHANGED
@@ -43,9 +43,10 @@
43
43
  * parse time (require 'comparator-NAME' not in KNOWN_CAPABILITIES).
44
44
  *
45
45
  * Match-type wildcards: `:matches` uses `*` (any sequence) and `?`
46
- * (one byte), per RFC 5228 §2.7.1. Both wildcards are converted to
47
- * a bounded RegExp built from escaped literal byte segments — no
48
- * user-controlled backtracking surface.
46
+ * (one byte), per RFC 5228 §2.7.1. The pattern is matched directly
47
+ * against the value rather than translated into a regular
48
+ * expression, so a script author cannot spend the delivery thread's
49
+ * CPU by writing a pattern that a backtracking engine explores.
49
50
  *
50
51
  * The interpreter does NOT execute multi-script chains, sieve
51
52
  * `include`s (RFC 6609), `notify` actions (RFC 5435), or `vacation`
@@ -63,7 +64,6 @@ var safeSieve = require("./safe-sieve");
63
64
  var { defineClass } = require("./framework-error");
64
65
  var numericBounds = require("./numeric-bounds");
65
66
  var validateOpts = require("./validate-opts");
66
- var codepointClass = require("./codepoint-class");
67
67
 
68
68
  var MailSieveError = defineClass("MailSieveError", { alwaysPermanent: true });
69
69
 
@@ -124,22 +124,64 @@ function _envelopeAddresses(env, key) {
124
124
 
125
125
  // ---- match-type ---------------------------------------------------------
126
126
 
127
- function _escapeRe(s) {
128
- return codepointClass.escapeRegExp(s);
127
+ // RFC 5228 §2.7.1 — `*` matches any sequence, `?` matches exactly one. Matched
128
+ // directly rather than translated into a regular expression.
129
+ //
130
+ // The translation is the obvious implementation and it is a denial of service.
131
+ // `*` becomes `.*`, so a pattern with N stars hands a backtracking engine N
132
+ // independent `.*` groups, and on a subject that never supplies the trailing
133
+ // literal the engine tries every way of dividing the subject between them: the
134
+ // cost is polynomial in the subject length with degree N. Three stars measured
135
+ // x7.7 per doubling of the subject, and ten stars — which a mailbox owner can
136
+ // type without meaning anything by it — did not finish on a 64-character
137
+ // Subject. A Sieve script is USER-authored in most deployments, and the gas
138
+ // budget does not help: gas counts operations, and one match is one operation
139
+ // however long the engine spends inside it.
140
+ //
141
+ // This walks the two strings with a single remembered wildcard position to fall
142
+ // back to. It is not linear: `*` followed by a long literal, matched against a
143
+ // subject that keeps almost matching it, re-walks the literal from each new
144
+ // alignment, so the worst case is the product of the two lengths. Measured at
145
+ // 3.9ms for a 500-character pattern against 2,000 characters, 11.4ms at
146
+ // 1,000 x 4,000, and 52.2ms at 2,000 x 8,000 — the quadratic shape, and a
147
+ // bound that both the script size and the header size already cap.
148
+ //
149
+ // What it is NOT is a function of how many wildcards the pattern contains,
150
+ // which is the property that mattered: the regex translation was polynomial
151
+ // with degree equal to the wildcard count, so a tenth `*` cost another factor
152
+ // of the subject length and a short Subject stopped returning at all.
153
+ // `i;ascii-casemap` folds US-ASCII A-Z to a-z and leaves every other character
154
+ // alone (RFC 4790 §9.2), which is also the only fold that preserves length.
155
+ // `String.prototype.toLowerCase` does not: `"İ".toLowerCase()` is two
156
+ // UTF-16 units, so folding the whole subject up front would shift every
157
+ // position after it and leave `?` — which matches exactly one character —
158
+ // facing two.
159
+ function _asciiLower(ch) {
160
+ var c = ch.charCodeAt(0);
161
+ return (c >= 0x41 && c <= 0x5A) ? String.fromCharCode(c + 0x20) : ch;
129
162
  }
130
163
 
131
- function _wildcardToRe(pattern, caseInsensitive) {
132
- // RFC 5228 §2.7.1 — `*` matches any sequence, `?` matches one. Escape
133
- // every other regex meta. Anchored both ends.
134
- var out = "^";
135
- for (var i = 0; i < pattern.length; i++) {
136
- var c = pattern[i];
137
- if (c === "*") out += ".*";
138
- else if (c === "?") out += ".";
139
- else out += _escapeRe(c);
140
- }
141
- out += "$";
142
- return new RegExp(out, caseInsensitive ? "i" : ""); // allow:dynamic-regex built from operator Sieve `:matches` pattern; every meta-char except `*`/`?` is regex-escaped, so the resulting NFA is linear in input length (no polynomial-backtrack surface)
164
+ function _wildcardMatches(pattern, subject, caseInsensitive) {
165
+ var pat = pattern, sub = subject;
166
+ var p = 0, s = 0;
167
+ var starP = -1, starS = 0;
168
+ function same(a, bChar) {
169
+ return caseInsensitive ? _asciiLower(a) === _asciiLower(bChar) : a === bChar;
170
+ }
171
+
172
+ while (s < sub.length) {
173
+ if (p < pat.length && (pat[p] === "?" || same(pat[p], sub[s]))) { p += 1; s += 1; continue; }
174
+ if (p < pat.length && pat[p] === "*") { starP = p; starS = s; p += 1; continue; }
175
+ // No match here. If a `*` came earlier, give it one more character and
176
+ // resume from just after it; otherwise the subject cannot match.
177
+ if (starP === -1) return false;
178
+ starS += 1;
179
+ s = starS;
180
+ p = starP + 1;
181
+ }
182
+ // Trailing stars can absorb the empty remainder; anything else cannot.
183
+ while (p < pat.length && pat[p] === "*") p += 1;
184
+ return p === pat.length;
143
185
  }
144
186
 
145
187
  function _matches(haystack, needle, matchType, comparator) {
@@ -155,7 +197,7 @@ function _matches(haystack, needle, matchType, comparator) {
155
197
  : haystack.indexOf(needle) !== -1;
156
198
  }
157
199
  if (matchType === "matches") {
158
- return _wildcardToRe(needle, ci).test(haystack);
200
+ return _wildcardMatches(needle, haystack, ci);
159
201
  }
160
202
  throw new MailSieveError("mail-sieve/bad-match-type",
161
203
  "unknown match-type: " + matchType);
@@ -61,6 +61,92 @@ function splitTagNameAttrs(inner, tailChars) {
61
61
  var HTML_TAG_NAME_TAIL = codepointClass.ASCII_ALNUM + ":-";
62
62
  var XML_TAG_NAME_TAIL = codepointClass.ASCII_ALNUM + ":-_";
63
63
 
64
+ // The five entities XML predefines. Everything else in an attribute value is
65
+ // either a numeric character reference or a reference to an entity a DTD
66
+ // declared, which nothing here expands: an attribute value is compared, not
67
+ // executed, and expanding declared entities is the door XXE comes through.
68
+ var XML_PREDEFINED_REFS = {
69
+ amp: "&", lt: "<", gt: ">", quot: "\"", apos: "'",
70
+ };
71
+
72
+ var DEC_DIGITS_RE = /^[0-9]+$/;
73
+ var HEX_DIGITS_RE = /^[0-9A-Fa-f]+$/;
74
+
75
+ // U+10FFFF is six hex digits and 1114111 is seven decimal ones.
76
+ var MAX_CHAR_REF_DIGITS = 7; // digit count, not bytes
77
+
78
+ // decodeCharRefs(s) — an attribute value with its numeric character references
79
+ // and predefined entities resolved, so a value can be COMPARED against what it
80
+ // denotes rather than against one spelling of it. `&#x73;vg` and `svg` are the
81
+ // same namespace to an XML processor, and a scanner that compares the lexical
82
+ // form disagrees with it.
83
+ //
84
+ // An unrecognized or malformed reference is left exactly as written rather than
85
+ // dropped, so nothing a caller then matches on can be manufactured by deleting
86
+ // characters. Returns the input unchanged when it holds no `&` at all.
87
+ function decodeCharRefs(s) {
88
+ if (s.indexOf("&") === -1) return s;
89
+ var out = "";
90
+ var i = 0;
91
+ while (i < s.length) {
92
+ var amp = s.indexOf("&", i);
93
+ if (amp === -1) { out += s.slice(i); break; }
94
+ out += s.slice(i, amp);
95
+ var semi = s.indexOf(";", amp + 1);
96
+ if (semi === -1) { out += s.slice(amp); break; }
97
+ var body = s.slice(amp + 1, semi);
98
+ var decoded = null;
99
+ if (body.charAt(0) === "#") {
100
+ var hex = body.charAt(1) === "x" || body.charAt(1) === "X";
101
+ var digits = hex ? body.slice(2) : body.slice(1);
102
+ // XML puts no limit on leading zeros, so what gets bounded is the
103
+ // SIGNIFICANT digits, not the lexical run: a cap on the written form
104
+ // refuses `&#0000000104;`, which an XML processor resolves normally.
105
+ // Skipping the zeros is a plain index walk, so the unbounded part of the
106
+ // input never reaches a pattern. The largest code point is six hex digits
107
+ // or seven decimal ones, and a refused reference is left exactly as
108
+ // written, so bounding it cannot manufacture a match.
109
+ var firstSignificant = 0;
110
+ while (firstSignificant < digits.length - 1 &&
111
+ digits.charAt(firstSignificant) === "0") firstSignificant += 1;
112
+ var significant = digits.slice(firstSignificant);
113
+ var wellFormed = significant.length > 0 &&
114
+ significant.length <= MAX_CHAR_REF_DIGITS &&
115
+ (hex ? HEX_DIGITS_RE.test(significant) : DEC_DIGITS_RE.test(significant));
116
+ if (wellFormed) {
117
+ var cp = parseInt(significant, hex ? 16 : 10);
118
+ // A lone surrogate is not a character; neither is anything past the
119
+ // last plane, nor U+0000, which XML's Char production excludes. Each
120
+ // would either throw out of fromCodePoint or put a value in the string
121
+ // that no document can contain.
122
+ if (cp > 0 && cp <= 0x10FFFF && !(cp >= 0xD800 && cp <= 0xDFFF)) {
123
+ decoded = String.fromCodePoint(cp);
124
+ }
125
+ }
126
+ } else if (Object.prototype.hasOwnProperty.call(XML_PREDEFINED_REFS, body)) {
127
+ decoded = XML_PREDEFINED_REFS[body];
128
+ }
129
+ out += decoded === null ? s.slice(amp, semi + 1) : decoded;
130
+ i = semi + 1;
131
+ }
132
+ return out;
133
+ }
134
+
135
+ // xmlCommentEnd(s, lt) — the same question for an XML document, where the
136
+ // answer is different. XML 1.0 §2.5 gives a comment exactly one terminator,
137
+ // "-->": there is no "--!>" and no abrupt "<!-->" / "<!--->" close. So the two
138
+ // grammars disagree in BOTH directions, and using the HTML reader on XML is a
139
+ // parser differential either way — it ends a comment early and reads the text
140
+ // after it as markup, and it ends one that XML says is still open.
141
+ //
142
+ // This module already names both tag-name grammars for the same reason; a
143
+ // caller picks the one its document is written in rather than restating it.
144
+ // Returns -1 when the comment is unterminated, as its HTML sibling does.
145
+ function xmlCommentEnd(s, lt) {
146
+ var end = s.indexOf("-->", lt + 4);
147
+ return end === -1 ? -1 : end + 3;
148
+ }
149
+
64
150
  // htmlCommentEnd(s, lt) — given that an HTML comment opens at index `lt`
65
151
  // (s.startsWith("<!--", lt)), return the index ONE PAST the comment's
66
152
  // terminator per the WHATWG HTML tokenizer, not just the legacy "-->" form.
@@ -233,6 +319,8 @@ module.exports = {
233
319
  scanToTagEnd: scanToTagEnd,
234
320
  splitTagNameAttrs: splitTagNameAttrs,
235
321
  htmlCommentEnd: htmlCommentEnd,
322
+ xmlCommentEnd: xmlCommentEnd,
323
+ decodeCharRefs: decodeCharRefs,
236
324
  HTML_TAG_NAME_TAIL: HTML_TAG_NAME_TAIL,
237
325
  XML_TAG_NAME_TAIL: XML_TAG_NAME_TAIL,
238
326
  isMarkupSpace: isMarkupSpace,
@@ -21,7 +21,7 @@
21
21
  "server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
22
22
  "browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
23
23
  },
24
- "refreshedAt": "2026-08-22T03:19:31.188Z"
24
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
25
25
  },
26
26
  "@noble/hashes": {
27
27
  "version": "2.3.0",
@@ -48,7 +48,7 @@
48
48
  "hashes": {
49
49
  "browser": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0"
50
50
  },
51
- "refreshedAt": "2026-08-22T03:19:31.188Z"
51
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
52
52
  },
53
53
  "@noble/curves": {
54
54
  "version": "2.3.0",
@@ -70,7 +70,7 @@
70
70
  "hashes": {
71
71
  "server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
72
72
  },
73
- "refreshedAt": "2026-08-22T03:19:31.188Z",
73
+ "refreshedAt": "2026-08-23T07:32:38.917Z",
74
74
  "components": {
75
75
  "@noble/hashes": {
76
76
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -114,7 +114,7 @@
114
114
  "server": "sha256:fab7ebe5737793862c473444f4ee5912f79dd1edec86683acbb4eecbca0f5892",
115
115
  "browser": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9"
116
116
  },
117
- "refreshedAt": "2026-08-22T03:19:31.188Z",
117
+ "refreshedAt": "2026-08-23T07:32:38.917Z",
118
118
  "components": {
119
119
  "@noble/hashes": {
120
120
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -148,7 +148,7 @@
148
148
  },
149
149
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
150
150
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
151
- "refreshedAt": "2026-08-22T03:19:31.188Z"
151
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
152
152
  },
153
153
  "bimi-trust-anchors": {
154
154
  "version": "operator-managed",
@@ -173,7 +173,7 @@
173
173
  },
174
174
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
175
175
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
176
- "refreshedAt": "2026-08-22T03:19:31.188Z"
176
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
177
177
  },
178
178
  "publicsuffix-list": {
179
179
  "version": "master",
@@ -193,10 +193,10 @@
193
193
  },
194
194
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
195
195
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
196
- "refreshedAt": "2026-08-22T03:19:31.188Z"
196
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
197
197
  },
198
198
  "@blamejs/pki": {
199
- "version": "0.5.25",
199
+ "version": "0.5.28",
200
200
  "license": "Apache-2.0",
201
201
  "author": "blamejs",
202
202
  "source": "https://github.com/blamejs/pki",
@@ -216,12 +216,12 @@
216
216
  "server": "lib/vendor/blamejs-pki.cjs"
217
217
  },
218
218
  "bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
219
- "bundledAt": "2026-08-21T00:00:00Z",
220
- "cpe": "cpe:2.3:a:blamejs:pki:0.5.25:*:*:*:*:node.js:*:*",
219
+ "bundledAt": "2026-08-23T00:00:00Z",
220
+ "cpe": "cpe:2.3:a:blamejs:pki:0.5.28:*:*:*:*:node.js:*:*",
221
221
  "hashes": {
222
- "server": "sha256:4e64654dfddb16742f615f24a4e8892286495d94ae29597cdc3e988122f3ef4a"
222
+ "server": "sha256:a11e84272034dc4065dac44f55b722b3b4e1803ddeb27b09cbe6d8644225209c"
223
223
  },
224
- "refreshedAt": "2026-08-22T03:19:31.188Z"
224
+ "refreshedAt": "2026-08-23T07:32:38.917Z"
225
225
  }
226
226
  }
227
227
  }