@blamejs/core 0.15.38 → 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 +2 -0
- package/lib/flag-targeting.js +2 -7
- package/lib/forms.js +9 -0
- package/lib/guard-regex.js +57 -0
- package/lib/mail-helo.js +14 -0
- package/lib/mcp.js +2 -2
- package/lib/middleware/bot-guard.js +8 -1
- package/lib/middleware/cors.js +5 -0
- package/lib/middleware/request-log.js +4 -0
- package/lib/middleware/span-http-server.js +10 -0
- package/lib/request-helpers.js +9 -0
- package/lib/self-update.js +14 -0
- package/lib/static.js +8 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ 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
|
+
|
|
11
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.
|
|
12
14
|
|
|
13
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).
|
package/lib/flag-targeting.js
CHANGED
|
@@ -178,14 +178,9 @@ function validateRules(rules, label) {
|
|
|
178
178
|
// bound does NOT defend ReDoS (`(a+)+$` is 6 chars); screen the pattern
|
|
179
179
|
// through b.guardRegex first — it refuses nested-quantifier / alternation-
|
|
180
180
|
// quantifier / lookaround-quantifier shapes before compilation.
|
|
181
|
+
guardRegex().assertSafe(cond.value, clabel + ".value", FlagError, "flag/bad-condition");
|
|
181
182
|
try {
|
|
182
|
-
guardRegex
|
|
183
|
-
} catch (ge) {
|
|
184
|
-
throw new FlagError("flag/bad-condition",
|
|
185
|
-
clabel + ".value: regex pattern rejected as unsafe (ReDoS shape) - " + ge.message);
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
// allow:dynamic-regex — operator targeting pattern, ReDoS-screened via guardRegex.sanitize (strict) + length-bounded to 200 chars above
|
|
183
|
+
// allow:dynamic-regex — operator targeting pattern, ReDoS-screened via guardRegex.assertSafe + length-bounded to 200 chars above
|
|
189
184
|
validatedCond._compiledRegex = new RegExp(cond.value);
|
|
190
185
|
} catch (e) {
|
|
191
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
|
package/lib/guard-regex.js
CHANGED
|
@@ -512,6 +512,61 @@ function gate(opts) {
|
|
|
512
512
|
// ---- adaptive integration-test fixtures (consumed by layer-5 host harness) ----
|
|
513
513
|
var INTEGRATION_FIXTURES = gateContract.identifierFixtures("^[a-z]+$", "(a+)+b");
|
|
514
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
|
+
|
|
515
570
|
// Assembled from the gate-contract guard factory: error class, registry
|
|
516
571
|
// exports (NAME / KIND / INTEGRATION_FIXTURES), buildProfile /
|
|
517
572
|
// compliancePosture / loadRulePack wiring, plus the per-guard inspection
|
|
@@ -530,3 +585,5 @@ var _guard = module.exports = gateContract.defineGuard({
|
|
|
530
585
|
intOpts: ["maxBytes", "maxPatternBytes", "maxBoundedRepeat", "maxConsecutiveStars"],
|
|
531
586
|
gate: gate,
|
|
532
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
|
@@ -648,10 +648,10 @@ function _validateValueAgainstSchema(value, schema, path) {
|
|
|
648
648
|
// (a `(a+)+$` pattern blows up at ~40 input chars). Screen the tool
|
|
649
649
|
// author's pattern through b.guardRegex so a ReDoS-shaped schema pattern
|
|
650
650
|
// can't pin a CPU when matched against request input.
|
|
651
|
-
try { guardRegex().
|
|
651
|
+
try { guardRegex().assertSafe(schema.pattern, path); }
|
|
652
652
|
catch (_ge) { return path + ": schema pattern rejected as unsafe (ReDoS shape)"; }
|
|
653
653
|
try {
|
|
654
|
-
var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern is ReDoS-screened via guardRegex.
|
|
654
|
+
var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern is ReDoS-screened via guardRegex.assertSafe above + input length-capped
|
|
655
655
|
if (!pat.test(value)) return path + ": does not match pattern";
|
|
656
656
|
}
|
|
657
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)
|
|
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 " +
|
package/lib/middleware/cors.js
CHANGED
|
@@ -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;
|
package/lib/request-helpers.js
CHANGED
|
@@ -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") {
|
package/lib/self-update.js
CHANGED
|
@@ -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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:861fafcf-aa1c-4e00-b8a0-ed9942340f9c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.39",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.15.39",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|