@blamejs/core 0.15.36 → 0.15.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/agent-posture-chain.js +1 -1
- package/lib/audit-chain.js +5 -1
- package/lib/cwt.js +6 -1
- package/lib/external-db.js +4 -3
- package/lib/flag-cache.js +8 -8
- package/lib/flag-targeting.js +14 -1
- package/lib/guard-regex.js +52 -3
- package/lib/inbox.js +18 -7
- package/lib/mail-arc-sign.js +6 -2
- package/lib/mail-auth.js +6 -1
- package/lib/mcp.js +9 -1
- package/lib/middleware/age-gate.js +2 -2
- package/lib/network-tls.js +5 -1
- package/lib/ws-client.js +22 -15
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.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
|
+
|
|
13
|
+
- 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).
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- v0.15.35 (2026-06-26) — **`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by.** The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{"route":"/api"}`) rather than `route=/api`. **Changed:** *shadowRegistry().snapshot() cardinality keys are canonical-JSON* — As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{"tenant":"a"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values. **Security:** *Label values can no longer forge extra Prometheus label pairs in the shadow registry* — b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render.
|
|
@@ -124,7 +124,7 @@ function create(opts) {
|
|
|
124
124
|
var auditImpl = opts.audit || audit();
|
|
125
125
|
var declaredRegimes = Object.create(null);
|
|
126
126
|
for (var i = 0; i < BUILTIN_REGIMES.length; i += 1) declaredRegimes[BUILTIN_REGIMES[i]] = true;
|
|
127
|
-
// allow:numeric-opt-Infinity — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT]; bad input falls back to default
|
|
127
|
+
// allow:numeric-opt-Infinity-intentional — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT] (the `<= DEFAULT_MAX_HOP_COUNT` upper bound rejects Infinity); bad input falls back to default
|
|
128
128
|
var maxHopCount = typeof opts.maxHopCount === "number" && opts.maxHopCount > 0 &&
|
|
129
129
|
opts.maxHopCount <= DEFAULT_MAX_HOP_COUNT
|
|
130
130
|
? Math.floor(opts.maxHopCount)
|
package/lib/audit-chain.js
CHANGED
|
@@ -37,6 +37,7 @@ var canonicalJson = require("./canonical-json");
|
|
|
37
37
|
var C = require("./constants");
|
|
38
38
|
var clusterStorage = require("./cluster-storage");
|
|
39
39
|
var frameworkSchema = require("./framework-schema");
|
|
40
|
+
var numericBounds = require("./numeric-bounds");
|
|
40
41
|
var sql = require("./sql");
|
|
41
42
|
var safeSql = require("./safe-sql");
|
|
42
43
|
var safeBuffer = require("./safe-buffer");
|
|
@@ -305,7 +306,10 @@ async function verifyChain(queryAllAsync, tableName, opts) {
|
|
|
305
306
|
// coerce so a Postgres INTEGER/BIGINT chainKey is type-stable in the
|
|
306
307
|
// reported break-shape and the per-key WHERE bind, matching SQLite.
|
|
307
308
|
var keyRows = frameworkSchema.coerceRows(await queryAllAsync(keysBuilt.sql, keysBuilt.params));
|
|
308
|
-
|
|
309
|
+
// Partition fan-out cap; a non-finite / <= 0 / non-integer value (Infinity
|
|
310
|
+
// would make the `keyRows.length > maxChains` cap unsatisfiable) falls back
|
|
311
|
+
// to the bounded default rather than disabling the cap.
|
|
312
|
+
var maxChains = numericBounds.isPositiveFiniteInt(opts.maxChains) ? opts.maxChains : 100000;
|
|
309
313
|
if (keyRows.length > maxChains) {
|
|
310
314
|
return {
|
|
311
315
|
ok: false,
|
package/lib/cwt.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
var cose = require("./cose");
|
|
40
40
|
var cbor = require("./cbor");
|
|
41
41
|
var C = require("./constants");
|
|
42
|
+
var numericBounds = require("./numeric-bounds");
|
|
42
43
|
var validateOpts = require("./validate-opts");
|
|
43
44
|
var { defineClass } = require("./framework-error");
|
|
44
45
|
|
|
@@ -191,7 +192,11 @@ async function verify(cwt, opts) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
// Time claims (NumericDate, seconds). Skew tolerance both directions.
|
|
194
|
-
|
|
195
|
+
// A present clockSkewSec must be a non-negative finite integer — an
|
|
196
|
+
// Infinity / NaN / negative skew would otherwise make `now > exp + skew`
|
|
197
|
+
// unsatisfiable and silently disable the expiry / not-before checks.
|
|
198
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.clockSkewSec, "cwt.verify: opts.clockSkewSec", CwtError, "cwt/bad-clock-skew");
|
|
199
|
+
var skew = (typeof opts.clockSkewSec === "number") ? opts.clockSkewSec : 60; // allow:raw-time-literal — clock-skew in seconds (NumericDate units), not a ms duration
|
|
195
200
|
var now = _nowSec(opts);
|
|
196
201
|
// A present exp / nbf MUST be a well-formed NumericDate — a non-numeric
|
|
197
202
|
// value would otherwise bypass the time check entirely (a token could
|
package/lib/external-db.js
CHANGED
|
@@ -1338,8 +1338,9 @@ async function transaction(fn, opts) {
|
|
|
1338
1338
|
// (serialization_failure) are transient — retry with capped attempts
|
|
1339
1339
|
// and a small jittered backoff. Operators tune retries via opts.deadlockRetries (default 3).
|
|
1340
1340
|
// numeric-bounds doesn't have a non-negative-int helper; use a
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1341
|
+
// Explicit isFinite/integer guard (zero is permitted to disable retries
|
|
1342
|
+
// entirely); a non-finite deadlockRetries is refused here, so Math.floor
|
|
1343
|
+
// below only ever sees a validated finite integer.
|
|
1343
1344
|
if (opts.deadlockRetries !== undefined) {
|
|
1344
1345
|
if (typeof opts.deadlockRetries !== "number" || !isFinite(opts.deadlockRetries) ||
|
|
1345
1346
|
opts.deadlockRetries < 0 || (opts.deadlockRetries | 0) !== opts.deadlockRetries) {
|
|
@@ -1348,7 +1349,7 @@ async function transaction(fn, opts) {
|
|
|
1348
1349
|
}
|
|
1349
1350
|
}
|
|
1350
1351
|
var maxRetries = (typeof opts.deadlockRetries === "number")
|
|
1351
|
-
? Math.floor(opts.deadlockRetries) : 3;
|
|
1352
|
+
? Math.floor(opts.deadlockRetries) : 3;
|
|
1352
1353
|
// Validate the transaction-level residency tag shape at entry (the
|
|
1353
1354
|
// sessionGucs / deadlockRetries discipline) so an empty-string tag
|
|
1354
1355
|
// fails before BEGIN rather than at the first statement.
|
package/lib/flag-cache.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
var validateOpts = require("./validate-opts");
|
|
25
25
|
var lazyRequire = require("./lazy-require");
|
|
26
26
|
var C = require("./constants");
|
|
27
|
+
var numericBounds = require("./numeric-bounds");
|
|
27
28
|
var { defineClass } = require("./framework-error");
|
|
28
29
|
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
29
30
|
|
|
@@ -36,18 +37,17 @@ function cache(downstream, opts) {
|
|
|
36
37
|
throw new FlagError("flag/bad-cache",
|
|
37
38
|
"cache: downstream provider must implement .evaluate()");
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// A present ttlMs / maxEntries must be a positive finite integer — Infinity
|
|
41
|
+
// would pass a bare `typeof === "number" && > 0` check and give a
|
|
42
|
+
// never-expiring entry (ttlMs) or an unbounded cache (maxEntries).
|
|
43
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.ttlMs, "flag.cache: opts.ttlMs", FlagError, "flag/bad-cache");
|
|
44
|
+
var ttlMs = (typeof opts.ttlMs === "number") ? opts.ttlMs : C.TIME.seconds(30);
|
|
43
45
|
if (ttlMs < C.TIME.seconds(1)) {
|
|
44
46
|
throw new FlagError("flag/bad-cache",
|
|
45
47
|
"cache: ttlMs must be >= 1000ms - got " + ttlMs);
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
var maxEntries = (typeof opts.maxEntries === "number"
|
|
49
|
-
? Math.floor(opts.maxEntries)
|
|
50
|
-
: 10000; // entry-count default
|
|
49
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxEntries, "flag.cache: opts.maxEntries", FlagError, "flag/bad-cache");
|
|
50
|
+
var maxEntries = (typeof opts.maxEntries === "number") ? opts.maxEntries : 10000; // entry-count default
|
|
51
51
|
var auditOn = opts.audit === true; // off by default — too chatty
|
|
52
52
|
var entries = new Map();
|
|
53
53
|
var hits = 0;
|
package/lib/flag-targeting.js
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,19 @@ 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.
|
|
174
181
|
try {
|
|
175
|
-
|
|
182
|
+
guardRegex().sanitize(cond.value, { profile: "strict" });
|
|
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
|
|
176
189
|
validatedCond._compiledRegex = new RegExp(cond.value);
|
|
177
190
|
} catch (e) {
|
|
178
191
|
throw new FlagError("flag/bad-condition",
|
package/lib/guard-regex.js
CHANGED
|
@@ -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" &&
|
|
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-
|
|
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
|
|
package/lib/inbox.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
var C = require("./constants");
|
|
46
46
|
var codepointClass = require("./codepoint-class");
|
|
47
|
+
var numericBounds = require("./numeric-bounds");
|
|
47
48
|
var lazyRequire = require("./lazy-require");
|
|
48
49
|
var safeJson = require("./safe-json");
|
|
49
50
|
var safeSql = require("./safe-sql");
|
|
@@ -155,15 +156,17 @@ function create(opts) {
|
|
|
155
156
|
// The table identifier reaches SQL through b.sql, which validates +
|
|
156
157
|
// quotes it by construction on every emitted statement; _validateTableName
|
|
157
158
|
// above fails fast at create() time on a bad name.
|
|
158
|
-
var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity
|
|
159
|
+
var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity-intentional — Infinity = retain indefinitely, a supported intent
|
|
159
160
|
? opts.retentionDays : 30; // default retention days
|
|
160
161
|
var auditOn = opts.audit !== false;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
// The byte / length caps must be positive finite integers — Infinity would
|
|
163
|
+
// disable the cap and admit unbounded stored payloads / identifiers.
|
|
164
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
165
|
+
["maxPayloadBytes", "messageIdMaxLen", "sourceMaxLen"],
|
|
166
|
+
"inbox.create", InboxError, "inbox/bad-opt");
|
|
167
|
+
var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number") ? opts.maxPayloadBytes : C.BYTES.kib(64);
|
|
168
|
+
var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number") ? opts.messageIdMaxLen : 256; // message-id length cap
|
|
169
|
+
var sourceMaxLen = (typeof opts.sourceMaxLen === "number") ? opts.sourceMaxLen : 256; // source length cap
|
|
167
170
|
|
|
168
171
|
function _emitAudit(action, outcome, metadata) {
|
|
169
172
|
if (!auditOn) return;
|
|
@@ -346,6 +349,14 @@ function create(opts) {
|
|
|
346
349
|
}
|
|
347
350
|
|
|
348
351
|
async function sweep() {
|
|
352
|
+
// retentionDays: Infinity is the documented "retain indefinitely" intent —
|
|
353
|
+
// there is no horizon to age past, so sweep deletes nothing. (Computing a
|
|
354
|
+
// cutoff would otherwise throw: `new Date(Date.now() - Infinity)` is an
|
|
355
|
+
// Invalid Date, and "Infinity days" is not a valid Postgres interval.)
|
|
356
|
+
if (!isFinite(retentionDays)) {
|
|
357
|
+
_emitAudit("inbox.swept", "success", { deleted: 0, retentionDays: retentionDays });
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
349
360
|
var dialect = _sqlDialect(externalDb);
|
|
350
361
|
var deleted = 0;
|
|
351
362
|
await externalDb.transaction(async function (xdb) {
|
package/lib/mail-arc-sign.js
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
var nodeCrypto = require("node:crypto");
|
|
52
52
|
var lazyRequire = require("./lazy-require");
|
|
53
53
|
var validateOpts = require("./validate-opts");
|
|
54
|
+
var numericBounds = require("./numeric-bounds");
|
|
54
55
|
var safeBuffer = require("./safe-buffer");
|
|
55
56
|
var dkim = require("./mail-dkim");
|
|
56
57
|
var { defineClass } = require("./framework-error");
|
|
@@ -246,8 +247,11 @@ function sign(opts) {
|
|
|
246
247
|
"sign: headersToSign[" + hi + "] must be a non-empty string");
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
// A present t= timestamp must be a positive finite integer (NumericDate
|
|
251
|
+
// seconds) — an Infinity / NaN value would serialize into a malformed t= tag.
|
|
252
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.timestamp, "arc.sign: opts.timestamp", MailAuthError, "arc-sign/bad-timestamp");
|
|
253
|
+
var timestamp = (typeof opts.timestamp === "number")
|
|
254
|
+
? opts.timestamp : Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
|
|
251
255
|
var auditOn = opts.audit !== false;
|
|
252
256
|
|
|
253
257
|
var keyObject;
|
package/lib/mail-auth.js
CHANGED
|
@@ -52,6 +52,7 @@ var structuredFields = require("./structured-fields");
|
|
|
52
52
|
var markupEscape = require("./markup-escape").markupEscape;
|
|
53
53
|
var bCrypto = require("./crypto");
|
|
54
54
|
var C = require("./constants");
|
|
55
|
+
var numericBounds = require("./numeric-bounds");
|
|
55
56
|
var dkim = require("./mail-dkim");
|
|
56
57
|
var mimeParse = require("./mime-parse");
|
|
57
58
|
var safeXml = require("./parsers/safe-xml");
|
|
@@ -1575,7 +1576,11 @@ async function arcVerify(rfc822, opts) {
|
|
|
1575
1576
|
var anyFail = false;
|
|
1576
1577
|
// RFC 8617 §5.2 — operator-tunable clock skew on t= (signing
|
|
1577
1578
|
// timestamp) and x= (expiration) tags. Default 5 min.
|
|
1578
|
-
|
|
1579
|
+
// A present clockSkewMs must be a non-negative finite integer; an Infinity /
|
|
1580
|
+
// NaN / negative skew makes `amsX + skewSec < nowSec` (and the t= future
|
|
1581
|
+
// check) unsatisfiable and silently disables the RFC 8617 §5.2 expiry /
|
|
1582
|
+
// future-timestamp gate. Non-finite falls to the default.
|
|
1583
|
+
var arcClockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
|
|
1579
1584
|
? opts.clockSkewMs : C.TIME.minutes(5);
|
|
1580
1585
|
var nowSec = Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
|
|
1581
1586
|
|
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().sanitize(schema.pattern, { profile: "strict" }); }
|
|
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
|
|
654
|
+
var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern is ReDoS-screened via guardRegex.sanitize (strict) 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"; }
|
|
@@ -101,9 +101,9 @@ function create(opts) {
|
|
|
101
101
|
"middleware.ageGate: opts.getAge must be a function (req) -> number | null");
|
|
102
102
|
}
|
|
103
103
|
var getAge = opts.getAge;
|
|
104
|
-
var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity — age is
|
|
104
|
+
var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (denies everyone), never a bypass
|
|
105
105
|
? opts.requireAge : null;
|
|
106
|
-
var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity — age threshold,
|
|
106
|
+
var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (classifies everyone below-threshold), never a bypass
|
|
107
107
|
? opts.consentRequired : null;
|
|
108
108
|
var hasParentalConsent = typeof opts.hasParentalConsent === "function" ? opts.hasParentalConsent : null;
|
|
109
109
|
var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
|
package/lib/network-tls.js
CHANGED
|
@@ -1204,7 +1204,11 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
1204
1204
|
// gets revoked, the attacker keeps presenting the cached "good" and
|
|
1205
1205
|
// the framework keeps accepting it. requireGood postures depend on
|
|
1206
1206
|
// freshness — reject expired or future-dated responses outright.
|
|
1207
|
-
|
|
1207
|
+
// A present clockSkewMs must be a non-negative finite integer; an Infinity /
|
|
1208
|
+
// NaN / negative skew would make the staleness check `now > nextUpdate + skew`
|
|
1209
|
+
// unsatisfiable and silently disable the freshness window — accepting a
|
|
1210
|
+
// replayed pre-revocation "good" response. Non-finite falls to the default.
|
|
1211
|
+
var clockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
|
|
1208
1212
|
? opts.clockSkewMs : C.TIME.minutes(5);
|
|
1209
1213
|
var now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
1210
1214
|
// thisUpdate / nextUpdate are already unix-ms NUMBERS (parseOcspResponse →
|
package/lib/ws-client.js
CHANGED
|
@@ -52,6 +52,7 @@ var { EventEmitter } = require("node:events");
|
|
|
52
52
|
|
|
53
53
|
var lazyRequire = require("./lazy-require");
|
|
54
54
|
var validateOpts = require("./validate-opts");
|
|
55
|
+
var numericBounds = require("./numeric-bounds");
|
|
55
56
|
var safeAsync = require("./safe-async");
|
|
56
57
|
var safeBuffer = require("./safe-buffer");
|
|
57
58
|
var bCrypto = lazyRequire(function () { return require("./crypto"); });
|
|
@@ -217,16 +218,18 @@ function connect(target, opts) {
|
|
|
217
218
|
"wsClient.connect: subprotocols[" + sp + "] must be a non-empty string");
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
var
|
|
229
|
-
|
|
221
|
+
// These are keepalive/timeout intervals and inbound-OOM caps; a present
|
|
222
|
+
// value must be a positive finite integer. A bare `typeof === "number" && > 0`
|
|
223
|
+
// check accepts Infinity, which silently disables the cap (a malicious server
|
|
224
|
+
// could then send an unbounded message/frame, or stall the handshake forever).
|
|
225
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
226
|
+
["pingMs", "pongMs", "maxMessageBytes", "maxFrameBytes", "handshakeTimeoutMs"],
|
|
227
|
+
"wsClient.connect", WsClientError, "ws-client/bad-opt");
|
|
228
|
+
var pingMs = (typeof opts.pingMs === "number") ? opts.pingMs : DEFAULT_PING_MS;
|
|
229
|
+
var pongMs = (typeof opts.pongMs === "number") ? opts.pongMs : DEFAULT_PONG_MS;
|
|
230
|
+
var maxMessageBytes = (typeof opts.maxMessageBytes === "number") ? opts.maxMessageBytes : DEFAULT_MAX_BYTES;
|
|
231
|
+
var maxFrameBytes = (typeof opts.maxFrameBytes === "number") ? opts.maxFrameBytes : DEFAULT_MAX_FRAME;
|
|
232
|
+
var handshakeTimeoutMs = (typeof opts.handshakeTimeoutMs === "number") ? opts.handshakeTimeoutMs : DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
230
233
|
|
|
231
234
|
var reconnectOpts = _normaliseReconnect(opts.reconnect);
|
|
232
235
|
var permessageDeflate = opts.permessageDeflate !== false;
|
|
@@ -281,14 +284,18 @@ function _normaliseReconnect(input) {
|
|
|
281
284
|
"wsClient.connect: reconnect must be false / null / object");
|
|
282
285
|
}
|
|
283
286
|
validateOpts(input, ["maxAttempts", "baseMs", "maxMs", "enabled"], "wsClient.connect.reconnect");
|
|
287
|
+
// baseMs / maxMs are backoff durations; a present value must be a positive
|
|
288
|
+
// finite integer (an Infinity base would stall the first reconnect forever).
|
|
289
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(input, ["baseMs", "maxMs"],
|
|
290
|
+
"wsClient.connect.reconnect", WsClientError, "ws-client/bad-reconnect-opt");
|
|
284
291
|
return {
|
|
285
292
|
enabled: input.enabled !== false,
|
|
286
|
-
maxAttempts:
|
|
293
|
+
// maxAttempts: Infinity is a deliberate "reconnect indefinitely" intent, so
|
|
294
|
+
// a non-finite value is accepted here (unlike the bounded caps above).
|
|
295
|
+
maxAttempts: (typeof input.maxAttempts === "number" && input.maxAttempts >= 0) // allow:numeric-opt-Infinity-intentional — Infinity = reconnect indefinitely, a supported intent
|
|
287
296
|
? input.maxAttempts : DEFAULT_RECONNECT_MAX_ATTEMPTS,
|
|
288
|
-
baseMs: (typeof input.baseMs === "number"
|
|
289
|
-
|
|
290
|
-
maxMs: (typeof input.maxMs === "number" && input.maxMs > 0) // allow:numeric-opt-Infinity
|
|
291
|
-
? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
|
|
297
|
+
baseMs: (typeof input.baseMs === "number") ? input.baseMs : DEFAULT_RECONNECT_BASE_MS,
|
|
298
|
+
maxMs: (typeof input.maxMs === "number") ? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
|
|
292
299
|
};
|
|
293
300
|
}
|
|
294
301
|
|
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:742c0889-e56f-4bee-8220-91a6cac8307b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-27T16:52:08.760Z",
|
|
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.38",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.38",
|
|
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.38",
|
|
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.38",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|