@blamejs/core 0.15.37 → 0.15.39

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.39 (2026-06-27) — **Nine more places that matched an operator-supplied regex against request data — User-Agent, Origin, request path, form fields, SMTP HELO, release-asset names — now screen the pattern for catastrophic backtracking (ReDoS) before use, and a new b.guardRegex.assertSafe helper makes that screening one call.** The previous release screened feature-flag and MCP regex patterns for ReDoS but did not sweep every place the framework matches an operator-supplied regex against attacker-controlled input. Nine more were found and fixed: the bot guard (User-Agent), CORS (Origin), the HTTP span middleware and the shared request skip-matcher used by CSRF / fetch-metadata / rate-limit / access-lock / age-gate and the request logger (request path), static serving (hashed-asset path pattern), form field validation (submitted field value), SMTP HELO generic-rDNS patterns (HELO name), and the self-updater's asset/signature patterns (names from a remote release feed). Each accepted an operator RegExp with only a type check and ran it on every matching request, so an accidentally catastrophic pattern such as (a+)+$ or ((a)+)+$ could pin a CPU on a crafted input — a length cap does not bound backtracking. Every one now screens the pattern through b.guardRegex at configuration time. A new b.guardRegex.assertSafe(input, label?, ErrorClass?, code?) primitive performs that screen in one call (accepting a RegExp or a pattern string), which operators can also use on their own patterns. **Added:** *b.guardRegex.assertSafe — screen a RegExp or pattern string for ReDoS in one call* — b.guardRegex.assertSafe(input, label?, ErrorClass?, code?, opts?) screens an already-compiled RegExp (its source) or a raw pattern string for the catastrophic-backtracking classes — nested, alternation-with, and lookaround quantifiers — throwing the supplied framework-error class (or the underlying GuardRegexError) on a hostile pattern and returning the input on success. It allows large or open-ended bounded repeats (`{8,}`, `{n,m}`): a single counted repeat matches in linear time and legitimate patterns (including the framework's own defaults) use them. It is the config-time guard used by the request-lifecycle fixes above, and operators can apply it to their own patterns before matching them against untrusted input. **Security:** *Operator regex patterns matched against request data are screened for ReDoS framework-wide* — An operator-supplied RegExp matched against attacker-controllable input is a denial-of-service surface if it has a catastrophic-backtracking shape: the input triggers exponential work in the regex engine. Nine sites accepted such patterns with only an `instanceof RegExp` type check and executed them per request — bot-guard against the User-Agent, CORS against the Origin header, the HTTP span middleware and the shared skip-path matcher (CSRF / fetch-metadata / rate-limit / access-lock / age-gate / request-log) against the request path, static serving against the request path, form validation against the submitted field value, SMTP HELO checks against the HELO name, and the self-updater against asset names from a remote release feed. Each now routes the pattern through b.guardRegex at configuration time, so a catastrophic shape is refused up front instead of being weaponized by a crafted request. A length bound on the input is not a defense: a nested-quantifier pattern backtracks catastrophically at a few dozen characters. **Detectors:** *Build guard: an operator regex matched against request input must be ReDoS-screened* — A codebase guard now fails the build if a primitive accepts an operator-supplied RegExp and executes it against request input without screening the pattern through b.guardRegex.assertSafe — so the catastrophic-backtracking class fixed in this release cannot be reintroduced at a new site (the trusted-input cases — local filesystem paths, operator config keys, operator-owned schemas — are explicitly allowlisted). · *Build guard: process.moduleLoadList filters must match the 'NativeModule X' naming* — A guard now fails the build if a test filters process.moduleLoadList by the 'node:X' name only. Node 20+ records a loaded builtin as 'NativeModule X', so a 'node:'-only filter in an edge-runtime no-eager-load test would rot green and miss a reintroduced top-level networking require.
12
+
13
+ - v0.15.38 (2026-06-27) — **Regex patterns supplied in feature-flag targeting rules and MCP tool input schemas are now screened for catastrophic-backtracking (ReDoS) shapes before compilation, so a pattern matched against request data can't pin a CPU.** Two places compiled a caller-supplied regex pattern and `.test()`'d it against request-controlled input with only a length bound as the stated defense: a feature-flag targeting condition (`op: "regex"`) matched against runtime attribute values, and an MCP tool's input-schema `pattern` matched against tool-call arguments. A length bound is not a ReDoS defense — a catastrophic-backtracking pattern such as `(a+)+$` is six characters and pins a CPU on a crafted input. Both patterns now pass through `b.guardRegex` (strict profile) before compilation, which refuses nested-quantifier, alternation-with-quantifier, and quantifier-inside-lookaround shapes. A ReDoS-shaped flag pattern is refused when the rules are validated; a ReDoS-shaped MCP schema pattern fails tool-input validation. Patterns built from the framework's own static tables, operator-owned JSON Schema patterns, the Sieve glob translator (which cannot express nested quantifiers), and the I-Regexp translator (linear by dialect) are unchanged. **Security:** *Feature-flag regex targeting conditions are screened for ReDoS before compilation* — A flag targeting rule with `op: "regex"` compiled the operator-supplied pattern and `.test()`'d it against runtime attribute values, guarded only by a 200-character length cap. Length does not bound catastrophic backtracking, so a pattern like `(a+)+$` combined with an attacker-controlled attribute value could pin a CPU during flag evaluation. The pattern is now screened through b.guardRegex (strict) when the rules are validated, and a catastrophic-backtracking shape is refused with a clear error. · *MCP tool input-schema patterns are screened for ReDoS before matching request input* — b.mcp.validateToolInput compiled a tool author's input-schema `pattern` and matched it against tool-call argument values; the 4096-character input cap does not bound backtracking (a `(a+)+$` pattern blows up at roughly forty input characters). The schema pattern is now screened through b.guardRegex (strict) before compilation, so a ReDoS-shaped pattern in a registered tool's schema fails input validation instead of letting hostile arguments hang the validator. · *b.guardRegex now catches wrapped nested-quantifier patterns* — The nested-quantifier detector matched a quantified group whose body contained a quantifier, but its inner match was paren-blind, so wrapping the inner quantifier in an extra group — `((a)+)+`, `(([a-z]+)*)*`, `((a+))+` — slipped past it while remaining catastrophic. A structural scan now tracks group nesting and refuses an unbounded-quantified group whose body itself contains an unbounded quantifier at any depth, so the wrapped forms are rejected alongside the bare `(a+)+`. Bounded repeats (`{n}`, `{n,m}`) are unaffected. This strengthens every b.guardRegex caller, including the flag-targeting and MCP screening above.
14
+
11
15
  - v0.15.37 (2026-06-27) — **Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures.** A number of configuration options validated a numeric value with a bare `typeof === "number" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation "good" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect "retry indefinitely", inbox "retain indefinitely" — continue to accept `Infinity`. **Security:** *A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks* — b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === "number" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP "good" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`. · *WebSocket-client inbound caps can no longer be disabled by an Infinity value* — b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate "reconnect indefinitely" intent). · *Inbox, flag-cache, and audit-chain caps reject a non-finite limit* — The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely).
12
16
 
13
17
  - v0.15.36 (2026-06-27) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed. **Detectors:** *Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers* — Each marked bare `.split(",")` / `.split(";")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed.
@@ -27,6 +27,8 @@
27
27
  */
28
28
 
29
29
  var validateOpts = require("./validate-opts");
30
+ var lazyRequire = require("./lazy-require");
31
+ var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
30
32
  var { defineClass } = require("./framework-error");
31
33
  var FlagError = defineClass("FlagError", { alwaysPermanent: true });
32
34
 
@@ -171,8 +173,14 @@ function validateRules(rules, label) {
171
173
  throw new FlagError("flag/bad-condition",
172
174
  clabel + ".value: regex pattern must be <= 200 chars (DoS defense)");
173
175
  }
176
+ // The compiled regex is .test()'d against runtime attribute values, so a
177
+ // catastrophic-backtracking (ReDoS) pattern is a DoS vector. A length
178
+ // bound does NOT defend ReDoS (`(a+)+$` is 6 chars); screen the pattern
179
+ // through b.guardRegex first — it refuses nested-quantifier / alternation-
180
+ // quantifier / lookaround-quantifier shapes before compilation.
181
+ guardRegex().assertSafe(cond.value, clabel + ".value", FlagError, "flag/bad-condition");
174
182
  try {
175
- // allow:dynamic-regex — operator-supplied targeting pattern, length-bounded to 200 chars above
183
+ // allow:dynamic-regex — operator targeting pattern, ReDoS-screened via guardRegex.assertSafe + length-bounded to 200 chars above
176
184
  validatedCond._compiledRegex = new RegExp(cond.value);
177
185
  } catch (e) {
178
186
  throw new FlagError("flag/bad-condition",
package/lib/forms.js CHANGED
@@ -34,6 +34,7 @@
34
34
  */
35
35
  var C = require("./constants");
36
36
  var { generateToken, timingSafeEqual } = require("./crypto");
37
+ var guardRegex = require("./guard-regex");
37
38
  var safeSchema = require("./safe-schema");
38
39
  var safeUrl = require("./safe-url");
39
40
  var template = require("./template");
@@ -198,6 +199,10 @@ function _renderInput(field) {
198
199
  "'.pattern must be a pre-compiled RegExp; got " +
199
200
  (typeof field.pattern) + ". Wrap the source string with `RegExp` at config time.");
200
201
  }
202
+ // Screen the operator-supplied pattern for catastrophic-backtracking
203
+ // (ReDoS) shapes at config time so a hostile-form-spec can't stage a
204
+ // pathological regex against the engine.
205
+ guardRegex.assertSafe(field.pattern, "forms: field[" + field.name + "].pattern");
201
206
  attrs.push('pattern="' + escapeAttribute(field.pattern.source) + '"');
202
207
  }
203
208
  if (field.min !== undefined) attrs.push('min="' + escapeAttribute(field.min) + '"');
@@ -490,6 +495,10 @@ function validate(spec, body) {
490
495
  "'.pattern must be a pre-compiled RegExp; got " +
491
496
  (typeof f.pattern) + ". Wrap the source string with `RegExp` at config time.");
492
497
  }
498
+ // Screen the operator-supplied pattern for catastrophic-backtracking
499
+ // (ReDoS) shapes before the test, so a pathological regex can't be
500
+ // run against the submitted value.
501
+ guardRegex.assertSafe(f.pattern, "forms: field[" + f.name + "].pattern");
493
502
  if (!f.pattern.test(coerced)) {
494
503
  errors[f.name] = f.errorMessages && f.errorMessages.pattern
495
504
  ? f.errorMessages.pattern
@@ -128,19 +128,68 @@ var DEFAULTS = gateContract.strictDefaults(PROFILES);
128
128
 
129
129
  var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256 });
130
130
 
131
+ // Structural nested-unbounded-quantifier detector. NESTED_QUANT_RE is paren-
132
+ // blind (its `[^()]*` can't span a nested group), so it misses WRAPPED forms
133
+ // like `((a)+)+` / `(([a-z]+)*)*` / `((a+))+` — adding one extra group around
134
+ // the inner quantifier bypasses the regex while the pattern stays catastrophic.
135
+ // This linear scan tracks group nesting and flags an unbounded-quantified group
136
+ // (`)+`, `)*`, `){n,}`) whose body itself contains an unbounded quantifier — the
137
+ // two-nested-unbounded-quantifier ReDoS class — at any group depth. Bounded
138
+ // repeats (`{n}`, `{n,m}`, `?`) are not unbounded, so they don't trip it (the
139
+ // large-bound case is handled separately by maxBoundedRepeat).
140
+ function _hasNestedQuantifier(src) {
141
+ var stack = []; // open groups: each { quant } — body has an unbounded quantifier
142
+ var inClass = false; // inside a [...] character class
143
+ var i = 0;
144
+ var n = src.length;
145
+ var UNBOUNDED_AFTER_GROUP = /^(?:[*+]\??|\{\d*,\})/; // )+ )* )+? )*? ){n,}
146
+ while (i < n) {
147
+ var c = src.charAt(i);
148
+ if (c === "\\") { i += 2; continue; } // escaped atom — skip both chars
149
+ if (inClass) { if (c === "]") inClass = false; i += 1; continue; }
150
+ if (c === "[") { inClass = true; i += 1; continue; }
151
+ if (c === "(") { stack.push({ quant: false }); i += 1; continue; }
152
+ if (c === ")") {
153
+ var grp = stack.pop() || { quant: false };
154
+ var qm = UNBOUNDED_AFTER_GROUP.exec(src.slice(i + 1)); // allow:regex-no-length-cap — bounded slice of a maxPatternBytes-capped input
155
+ var closeUnbounded = qm !== null;
156
+ if (grp.quant && closeUnbounded) return true; // nested unbounded quantifier → catastrophic
157
+ // The closing group contributes an unbounded quantifier to its PARENT's
158
+ // body if its own body had one, or if it is itself unbounded-quantified.
159
+ if (stack.length && (grp.quant || closeUnbounded)) stack[stack.length - 1].quant = true;
160
+ i += 1 + (qm ? qm[0].length : 0);
161
+ continue;
162
+ }
163
+ if (c === "*" || c === "+") { // unbounded quantifier on the preceding atom
164
+ if (stack.length) stack[stack.length - 1].quant = true;
165
+ i += 1; continue;
166
+ }
167
+ if (c === "{") {
168
+ var open = /^\{\d*,\}/.exec(src.slice(i)); // allow:regex-no-length-cap — bounded slice // {n,} unbounded
169
+ if (open) { if (stack.length) stack[stack.length - 1].quant = true; i += open[0].length; continue; }
170
+ var bounded = /^\{\d+(?:,\d+)?\}/.exec(src.slice(i)); // allow:regex-no-length-cap — bounded slice // {n} / {n,m} bounded
171
+ if (bounded) { i += bounded[0].length; continue; }
172
+ i += 1; continue; // literal `{`
173
+ }
174
+ i += 1;
175
+ }
176
+ return false;
177
+ }
178
+
131
179
 
132
180
  function _detectIssues(input, opts) {
133
181
  var pre = gateContract.detectStringInput(input, opts, { name: "regex", noun: "regex pattern", cap: { bytes: opts.maxPatternBytes, kind: "pattern-cap", snippet: "regex pattern exceeds maxPatternBytes " + opts.maxPatternBytes } });
134
182
  if (pre.done) return pre.issues;
135
183
  var issues = pre.issues;
136
184
 
137
- if (opts.nestedQuantPolicy !== "allow" && NESTED_QUANT_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
185
+ if (opts.nestedQuantPolicy !== "allow" &&
186
+ (NESTED_QUANT_RE.test(input) || _hasNestedQuantifier(input))) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
138
187
  issues.push({
139
188
  kind: "nested-quantifier", severity: "critical",
140
189
  ruleId: "regex.nested-quantifier",
141
190
  snippet: "pattern contains nested-quantifier shape (e.g. " +
142
- "`(a+)+`) — canonical ReDoS catastrophic-backtracking " +
143
- "class (CVE-2024-21538 cross-spawn / CVE-2022-25929)",
191
+ "`(a+)+` / `((a)+)+`) — canonical ReDoS catastrophic-" +
192
+ "backtracking class (CVE-2024-21538 cross-spawn / CVE-2022-25929)",
144
193
  });
145
194
  }
146
195
 
@@ -463,6 +512,61 @@ function gate(opts) {
463
512
  // ---- adaptive integration-test fixtures (consumed by layer-5 host harness) ----
464
513
  var INTEGRATION_FIXTURES = gateContract.identifierFixtures("^[a-z]+$", "(a+)+b");
465
514
 
515
+ /**
516
+ * @primitive b.guardRegex.assertSafe
517
+ * @signature b.guardRegex.assertSafe(input, label?, ErrorClass?, code?, opts?)
518
+ * @since 0.15.39
519
+ * @status stable
520
+ * @related b.guardRegex.sanitize, b.guardRegex.validate
521
+ *
522
+ * Screen an already-compiled <code>RegExp</code> (or a raw pattern string) for
523
+ * catastrophic-backtracking (ReDoS) shapes, throwing if the pattern is unsafe.
524
+ * This is the config-time guard for request-lifecycle code that matches an
525
+ * operator-supplied regex against attacker-controlled input (User-Agent,
526
+ * Origin, request path, form field, HELO) — an accidentally-catastrophic
527
+ * operator pattern would otherwise be a per-request DoS once a hostile input
528
+ * triggers the backtracking.
529
+ *
530
+ * Pass a <code>RegExp</code> instance (its <code>.source</code> is screened) or
531
+ * a pattern string. On a hostile shape it throws <code>ErrorClass(code, ...)</code>
532
+ * when an error class is supplied, otherwise the underlying
533
+ * <code>GuardRegexError</code>. Returns the input unchanged on success.
534
+ *
535
+ * By default it rejects the catastrophic-backtracking classes — nested,
536
+ * alternation-with, and lookaround quantifiers — but ALLOWS large/open bounded
537
+ * repeats (<code>{8,}</code>, <code>{n,m}</code>): a single counted repeat is
538
+ * linear, not exponential, and legitimate patterns (e.g. a hex hash of 8+
539
+ * digits) use them. Pass an explicit <code>opts</code> to override.
540
+ *
541
+ * @opts
542
+ * profile: string, // guardRegex profile (default: "strict")
543
+ * boundedRepeatPolicy: string, // default: "allow" (large bounded repeats are linear)
544
+ *
545
+ * @example
546
+ * b.guardRegex.assertSafe(/^[a-z]+$/); // ok — returns the RegExp
547
+ * b.guardRegex.assertSafe(/\.[a-f0-9]{8,}\./); // ok — a single bounded repeat is linear
548
+ * try { b.guardRegex.assertSafe(/((a)+)+$/); } // throws — nested quantifier
549
+ * catch (e) { e.code; } // → "regex/unsafe-pattern"
550
+ */
551
+ function assertSafe(input, label, ErrorClass, code, opts) {
552
+ var source = (input instanceof RegExp) ? input.source : input;
553
+ try {
554
+ // Screen the catastrophic-backtracking classes (nested / alternation /
555
+ // lookaround quantifiers — held at every profile) but allow large bounded
556
+ // repeats: a counted repeat matches in linear time, and rejecting `{n,}`
557
+ // would refuse legitimate operator patterns (and the framework's own
558
+ // defaults, e.g. b.staticServe.DEFAULT_HASHED_PATTERN's `{8,}`).
559
+ _guard.sanitize(source, opts || { profile: "strict", boundedRepeatPolicy: "allow" });
560
+ } catch (e) {
561
+ if (ErrorClass) {
562
+ throw new ErrorClass(code || "regex/unsafe-pattern",
563
+ (label || "regex") + ": pattern rejected as unsafe (ReDoS shape) - " + (e && e.message));
564
+ }
565
+ throw e;
566
+ }
567
+ return input;
568
+ }
569
+
466
570
  // Assembled from the gate-contract guard factory: error class, registry
467
571
  // exports (NAME / KIND / INTEGRATION_FIXTURES), buildProfile /
468
572
  // compliancePosture / loadRulePack wiring, plus the per-guard inspection
@@ -481,3 +585,5 @@ var _guard = module.exports = gateContract.defineGuard({
481
585
  intOpts: ["maxBytes", "maxPatternBytes", "maxBoundedRepeat", "maxConsecutiveStars"],
482
586
  gate: gate,
483
587
  });
588
+
589
+ _guard.assertSafe = assertSafe;
package/lib/mail-helo.js CHANGED
@@ -105,6 +105,7 @@ var { defineClass } = require("./framework-error");
105
105
  var lazyRequire = require("./lazy-require");
106
106
  var ipUtils = require("./ip-utils");
107
107
  var gateContract = require("./gate-contract");
108
+ var guardRegex = require("./guard-regex");
108
109
 
109
110
  var audit = lazyRequire(function () { return require("./audit"); });
110
111
 
@@ -210,6 +211,19 @@ async function evaluate(ctx, opts) {
210
211
  var selfNames = (opts.selfNames || []).map(function (n) { return String(n).toLowerCase(); });
211
212
  var auditImpl = opts.audit || audit();
212
213
 
214
+ // Screen operator-supplied generic-rDNS patterns ONCE at build time —
215
+ // they are .test()'d per-match against attacker-controlled rDNS / claim
216
+ // names, so a catastrophic-backtracking shape would be a ReDoS lever.
217
+ if (Array.isArray(opts.genericRdnsPatterns)) {
218
+ for (var gi = 0; gi < opts.genericRdnsPatterns.length; gi += 1) {
219
+ var gre = opts.genericRdnsPatterns[gi];
220
+ if (gre instanceof RegExp) {
221
+ guardRegex.assertSafe(gre, "mail.helo.evaluate: genericRdnsPatterns[" + gi + "]",
222
+ MailHeloError, "mail-helo/unsafe-pattern");
223
+ }
224
+ }
225
+ }
226
+
213
227
  if (!ctx || typeof ctx !== "object") {
214
228
  throw new MailHeloError("mail-helo/bad-input",
215
229
  "evaluate: ctx must be a plain object");
package/lib/mcp.js CHANGED
@@ -37,6 +37,8 @@ var safeJson = require("./safe-json");
37
37
  var safeBuffer = require("./safe-buffer");
38
38
  var requestHelpers = require("./request-helpers");
39
39
  var audit = require("./audit");
40
+ var lazyRequire = require("./lazy-require");
41
+ var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
40
42
  var { McpError } = require("./framework-error");
41
43
 
42
44
  var TOOL_NAME_MAX = 64; // string-length cap, not bytes
@@ -642,8 +644,14 @@ function _validateValueAgainstSchema(value, schema, path) {
642
644
  // cost scales with the number of code units the engine scans, so 4096
643
645
  // chars is the correct ReDoS bound regardless of UTF-8 byte size.
644
646
  if (value.length > 4096) return path + ": value exceeds 4096-char cap before regex test"; // ReDoS char cap (not bytes)
647
+ // The input-length cap above does NOT bound catastrophic backtracking
648
+ // (a `(a+)+$` pattern blows up at ~40 input chars). Screen the tool
649
+ // author's pattern through b.guardRegex so a ReDoS-shaped schema pattern
650
+ // can't pin a CPU when matched against request input.
651
+ try { guardRegex().assertSafe(schema.pattern, path); }
652
+ catch (_ge) { return path + ": schema pattern rejected as unsafe (ReDoS shape)"; }
645
653
  try {
646
- var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern from registered tool author, not request input; bounded above
654
+ var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern is ReDoS-screened via guardRegex.assertSafe above + input length-capped
647
655
  if (!pat.test(value)) return path + ": does not match pattern";
648
656
  }
649
657
  catch (_e) { return path + ": invalid pattern in schema"; }
@@ -54,6 +54,7 @@ var validateOpts = require("../validate-opts");
54
54
  var denyResponse = require("./deny-response").denyResponse;
55
55
  var { defineClass } = require("../framework-error");
56
56
  var audit = lazyRequire(function () { return require("../audit"); });
57
+ var guardRegex = lazyRequire(function () { return require("../guard-regex"); });
57
58
 
58
59
  var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
59
60
 
@@ -64,7 +65,13 @@ var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
64
65
  // constructing patterns dynamically compile at their own call site so
65
66
  // the pattern source is visible in their code.
66
67
  function _coerceAgentPattern(r, where) {
67
- if (r instanceof RegExp) return r;
68
+ if (r instanceof RegExp) {
69
+ // Screen the operator's pattern for catastrophic-backtracking (ReDoS)
70
+ // shapes ONCE at create()-time — it is later .test()'d against the
71
+ // attacker-controlled User-Agent on every request.
72
+ guardRegex().assertSafe(r, where, BotGuardError, "bot-guard/unsafe-pattern");
73
+ return r;
74
+ }
68
75
  throw new BotGuardError("bot-guard/bad-pattern",
69
76
  where + " must be a RegExp instance; got " + (typeof r) +
70
77
  " (compile the pattern at the call site so the source is visible " +
@@ -52,6 +52,7 @@
52
52
  var C = require("../constants");
53
53
  var lazyRequire = require("../lazy-require");
54
54
  var audit = lazyRequire(function () { return require("../audit"); });
55
+ var guardRegex = lazyRequire(function () { return require("../guard-regex"); });
55
56
  var requestHelpers = require("../request-helpers");
56
57
  var safeUrl = require("../safe-url");
57
58
  var validateOpts = require("../validate-opts");
@@ -237,6 +238,10 @@ function create(opts) {
237
238
  }
238
239
  origins.push({ kind: "string", canonical: canonEntry, original: entry });
239
240
  } else if (entry instanceof RegExp) {
241
+ // Screen the operator's RegExp once at create() time for
242
+ // catastrophic-backtracking (ReDoS) shapes before it ever runs
243
+ // .test() against an attacker-controlled Origin header per request.
244
+ guardRegex().assertSafe(entry, "middleware.cors: origins[" + oi + "]", CorsError, "cors/unsafe-pattern");
240
245
  origins.push({ kind: "regex", pattern: entry });
241
246
  } else {
242
247
  throw new CorsError("cors/bad-origin",
@@ -29,6 +29,7 @@
29
29
  * for fully custom logic (e.g. "warn" only on slow-path requests).
30
30
  */
31
31
  var C = require("../constants");
32
+ var guardRegex = require("../guard-regex");
32
33
  var requestHelpers = require("../request-helpers");
33
34
  var validateOpts = require("../validate-opts");
34
35
 
@@ -95,6 +96,9 @@ function create(opts) {
95
96
  if (typeof skipPaths[i] !== "string" && !(skipPaths[i] instanceof RegExp)) {
96
97
  throw new Error("middleware.requestLog: skipPaths[" + i + "] must be a string prefix or RegExp");
97
98
  }
99
+ if (skipPaths[i] instanceof RegExp) {
100
+ guardRegex.assertSafe(skipPaths[i], "middleware.requestLog: skipPaths[" + i + "]");
101
+ }
98
102
  }
99
103
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
100
104
  ? opts.trustProxy : false;
@@ -49,6 +49,7 @@ var { defineClass } = require("../framework-error");
49
49
  var SpanHttpError = defineClass("SpanHttpError", { alwaysPermanent: true });
50
50
 
51
51
  var observability = lazyRequire(function () { return require("../observability"); });
52
+ var guardRegex = lazyRequire(function () { return require("../guard-regex"); });
52
53
 
53
54
  function _shouldIgnore(path, ignorePaths) {
54
55
  if (!ignorePaths || !Array.isArray(ignorePaths)) return false;
@@ -177,6 +178,15 @@ function create(opts) {
177
178
  var tracer = opts.tracer;
178
179
  var onEnd = opts.onEnd || null;
179
180
  var ignorePaths = opts.ignorePaths || null;
181
+ if (Array.isArray(ignorePaths)) {
182
+ for (var ip = 0; ip < ignorePaths.length; ip++) {
183
+ if (ignorePaths[ip] instanceof RegExp) {
184
+ guardRegex().assertSafe(ignorePaths[ip],
185
+ "middleware.spanHttpServer: ignorePaths[" + ip + "]",
186
+ SpanHttpError, "span-http/unsafe-pattern");
187
+ }
188
+ }
189
+ }
180
190
  var captureReqHeaders = opts.captureRequestHeaders || null;
181
191
  var captureResHeaders = opts.captureResponseHeaders || null;
182
192
  var spanNameFn = opts.spanNameFn || null;
@@ -49,6 +49,9 @@ var lazyRequire = require("./lazy-require");
49
49
  // required very early in the boot graph. Only touched at middleware-construction
50
50
  // time by trustedClientIp(), never on the hot path.
51
51
  var _ssrfGuard = lazyRequire(function () { return require("./ssrf-guard"); });
52
+ // Lazy for the same boot-graph reason — only touched at middleware-construction
53
+ // time by makeSkipMatcher() to screen operator-supplied skip RegExps for ReDoS.
54
+ var _guardRegex = lazyRequire(function () { return require("./guard-regex"); });
52
55
 
53
56
  var HTTP_STATUS = Object.freeze({
54
57
  OK: 0xC8,
@@ -991,6 +994,12 @@ function makeSkipMatcher(opts, label) {
991
994
  throw new TypeError(label + ": skipPaths[" + i + "] must be a string prefix or RegExp, got " +
992
995
  typeof skipPaths[i]);
993
996
  }
997
+ // Screen operator-supplied skip RegExps for catastrophic-backtracking
998
+ // shapes once at construction time — they later run .test() against
999
+ // attacker-controlled request paths on the hot path.
1000
+ if (skipPaths[i] instanceof RegExp) {
1001
+ _guardRegex().assertSafe(skipPaths[i], label + ": skipPaths[" + i + "]");
1002
+ }
994
1003
  }
995
1004
  var skipFn = opts.skip;
996
1005
  if (skipFn !== undefined && skipFn !== null && typeof skipFn !== "function") {
@@ -54,6 +54,7 @@ var numericBounds = require("./numeric-bounds");
54
54
  var atomicFile = require("./atomic-file");
55
55
  var validateOpts = require("./validate-opts");
56
56
  var bCrypto = require("./crypto");
57
+ var guardRegex = require("./guard-regex");
57
58
  var httpClient = require("./http-client");
58
59
  var safeJson = require("./safe-json");
59
60
  var { URL: NodeUrl } = require("node:url");
@@ -242,12 +243,25 @@ function _validatePollOpts(opts) {
242
243
  throw new SelfUpdateError("selfupdate/bad-asset-pattern",
243
244
  "selfUpdate.poll: opts.assetPattern must be a RegExp or string when present");
244
245
  }
246
+ // Screen an operator-supplied RegExp once at config-time; it is
247
+ // later .test()'d against attacker-controlled asset names in the
248
+ // request path, so a catastrophic-backtracking shape would be a
249
+ // per-request DoS. The string form is matched by substring
250
+ // (indexOf), never compiled, so it carries no ReDoS risk.
251
+ if (value instanceof RegExp) {
252
+ guardRegex.assertSafe(value, "selfUpdate: assetPattern",
253
+ SelfUpdateError, "selfupdate/unsafe-asset-pattern");
254
+ }
245
255
  },
246
256
  signaturePattern: function (value) {
247
257
  if (value !== undefined && !(value instanceof RegExp) && typeof value !== "string") {
248
258
  throw new SelfUpdateError("selfupdate/bad-sig-pattern",
249
259
  "selfUpdate.poll: opts.signaturePattern must be a RegExp or string when present");
250
260
  }
261
+ if (value instanceof RegExp) {
262
+ guardRegex.assertSafe(value, "selfUpdate: signaturePattern",
263
+ SelfUpdateError, "selfupdate/unsafe-sig-pattern");
264
+ }
251
265
  },
252
266
  maxBytes: function (value) {
253
267
  numericBounds.requirePositiveFiniteIntIfPresent(value,
package/lib/static.js CHANGED
@@ -60,6 +60,7 @@ var observability = lazyRequire(function () { return require("./observability");
60
60
  // import cycles. Operators opt out via contentSafety: null (audited).
61
61
  var guardAll = lazyRequire(function () { return require("./guard-all"); });
62
62
  var guardFilename = lazyRequire(function () { return require("./guard-filename"); });
63
+ var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
63
64
 
64
65
  var _err = StaticServeError.factory;
65
66
 
@@ -521,6 +522,13 @@ function _validateCreateOpts(opts) {
521
522
  if (value !== undefined && value !== null && !(value instanceof RegExp)) {
522
523
  throw errorClass.factory(code, "staticServe.create: hashedPathPattern must be a RegExp");
523
524
  }
525
+ // Screen the operator-supplied pattern once at create() time — it is
526
+ // .test()'d against the attacker-controlled request path on every
527
+ // download, so a catastrophic-backtracking (ReDoS) shape would be a
528
+ // per-request DoS. Reject the unsafe pattern at config time instead.
529
+ if (value instanceof RegExp) {
530
+ guardRegex().assertSafe(value, "staticServe: hashedPathPattern", StaticServeError, "static/unsafe-pattern");
531
+ }
524
532
  },
525
533
  // indexFile === null is the operator's "disable" sentinel; the helper
526
534
  // returns null/undefined unchanged so we keep that semantic.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.37",
3
+ "version": "0.15.39",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:a1be0521-cb1d-4b8e-9ed6-b0053b5b9bfd",
5
+ "serialNumber": "urn:uuid:861fafcf-aa1c-4e00-b8a0-ed9942340f9c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-27T09:36:13.300Z",
8
+ "timestamp": "2026-06-28T04:47:31.828Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.15.37",
22
+ "bom-ref": "@blamejs/core@0.15.39",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.15.37",
25
+ "version": "0.15.39",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.15.37",
29
+ "purl": "pkg:npm/%40blamejs/core@0.15.39",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.15.37",
57
+ "ref": "@blamejs/core@0.15.39",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]