@blamejs/core 0.10.1 → 0.10.2
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 +1 -0
- package/lib/auth/sd-jwt-vc.js +11 -0
- package/lib/cli.js +13 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-xml.js +39 -1
- package/lib/otel-export.js +13 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.10.x
|
|
10
10
|
|
|
11
|
+
- v0.10.2 (2026-05-16) — **CVE backstops layered on top of v0.10.0.** Five additional refusals across `b.guardRegex`, `b.otelExport`, `b.guardXml`, `b.guardGraphql`, plus a host-side ingress route for `b.cli`. Every change is opt-out (refusal at every profile); no API removals. **(a) `b.guardRegex` glob-shape detectors with explicit `inputKind` gate** — new `consecutiveStarPolicy` + `nestedExtglobPolicy` (defaults `"reject"`) + `maxConsecutiveStars` (default 2) + `inputKind: "regex" | "glob"` (default `"regex"`). The glob-shape detectors fire ONLY when the caller passes `inputKind: "glob"` — ECMAScript regex syntax cannot produce `***` (SyntaxError) and the extglob heads `*(`/`+(`/`?(`/`@(`/`!(` collide with valid `quantifier + capturing group` shapes, so applying these detectors to regex inputs is false-positive territory. Callers handling glob fragments (picomatch / micromatch-style patterns) opt in via `inputKind: "glob"` and get refusals for ≥3 consecutive `*` metacharacters ([CVE-2026-26996](https://nvd.nist.gov/vuln/detail/CVE-2026-26996) — O(4^N) backtracking on non-matching literal) and for any extglob whose body contains another extglob ([CVE-2026-33671](https://nvd.nist.gov/vuln/detail/CVE-2026-33671) — picomatch nested-quantifier backtracking). `**` recursive-glob stays permitted under `maxConsecutiveStars: 2`. **(b) `b.cli --ignore` ReDoS ingress closure** — `cli --ignore <pattern>` arguments route through `b.guardRegex.sanitize({ profile: "strict" })` before reaching `new RegExp(pattern)`. Strict-profile refusal of nested-quantifier / lookaround-quantifier / unbounded-bounded-repeat shapes still applies in default `inputKind: "regex"` mode, closing the host-side surface for the classic ReDoS classes. **(c) `b.otelExport.flush()` response cap** — every outbound OTLP request now pins `maxResponseBytes: 1 MiB` + a typed `errorClass`, so a malicious / misconfigured collector cannot exhaust memory in the export loop ([CVE-2026-40891](https://nvd.nist.gov/vuln/detail/CVE-2026-40891) / [CVE-2026-40182](https://nvd.nist.gov/vuln/detail/CVE-2026-40182) class). **(d) `b.guardXml` numeric-character-reference fan-out cap** — new `maxNumericCharRefs` opt (strict 1024 / balanced 16384 / permissive 262144). NCRs are counted independently of `entityPolicy`, so a signed-XML path that legitimately permits entity expansion cannot accidentally disable the NCR cap ([CVE-2026-26278](https://nvd.nist.gov/vuln/detail/CVE-2026-26278) / [CVE-2026-33036](https://nvd.nist.gov/vuln/detail/CVE-2026-33036) — billion-NCR fan-out class). **(e) `b.guardGraphql` prototype-pollution refusal** — refuses `__proto__` / `constructor` / `prototype` as top-level variable keys (`Object.prototype.hasOwnProperty.call(variables, ...)` check, sidesteps a poisoned-prototype `in` lookup) AND as field / alias / `$variable` identifiers in the query body, including the no-whitespace alias form `query { a:__proto__ }` (the colon is a valid identifier-position prefix). Refused at every profile, severity `critical` ([CVE-2026-32621](https://nvd.nist.gov/vuln/detail/CVE-2026-32621) class). **(f) `b.auth.sdJwtVc.present()` defense-in-depth comment** — documents that the holder-side pre-parse of `_sd_alg` reads from unsigned bytes safely because `verify()` re-parses from the cryptographically-verified signing input; no behavioral change. **Regression coverage** — `test/fixtures/exploit-corpus/corpus.json` gains four entries: glob-mode positive refusal for `***+nonmatch` and `*(*(a))`, regex-mode pass for `a*(b+(c))` (false-positive class the design refused to ship), and the colon-prefix GraphQL alias `query { a:__proto__ }`. **Operator impact:** existing operators see no change in default behavior — the new glob detectors are opt-in via `inputKind: "glob"`. Operators wiring `b.guardRegex` over glob fragments (file-pattern allowlists, rsync-style rules) opt in and get the CVE-2026-26996 / -33671 refusals; opt back out per call via `consecutiveStarPolicy: "allow"` / `nestedExtglobPolicy: "allow"`. `b.guardXml` operators on signed-XML pipelines opt out via `maxNumericCharRefs: Infinity` if they bound NCRs upstream. GraphQL variable / query-body refusals are not opt-out — `__proto__` / `constructor` / `prototype` are never legitimate identifiers in operator-supplied input. References: [picomatch CVE-2024-4067 family](https://nvd.nist.gov/vuln/detail/CVE-2024-4067), [OWASP ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS), [OWASP XXE / Billion Laughs](https://owasp.org/www-community/vulnerabilities/XML_Entity_Expansion), [GraphQL Server Security Best Practices](https://www.apollographql.com/docs/router/configuration/overview/).
|
|
11
12
|
- v0.10.1 (2026-05-16) — **First npm-published v0.10.x artifact.** v0.10.0 was tagged + released on GitHub but its npm-publish workflow OOM'd at the lint+smoke gate (default Node ~4GB heap couldn't load the expanded mail-stack + audit-fix test surface). v0.10.1 adds `NODE_OPTIONS=--max-old-space-size=8192` to the workflow's smoke step so the parent process gets the same headroom the forked test workers already get. No runtime / API changes from v0.10.0 — every primitive, posture, and security default ships exactly as documented in the v0.10.0 release notes below. Operators who fetched the v0.10.0 git tag can re-tag from v0.10.1 (`git fetch origin v0.10.1`) to land on the npm-published commit; the framework code itself is byte-identical apart from the workflow file + version bump.
|
|
12
13
|
- v0.10.0 (2026-05-16) — **Mail-stack feature-complete + cross-surface hardening.** Bundled minor closing the blamepost mail-stack roadmap (five new operator-facing namespaces) plus a multi-domain hardening sweep across auth / crypto / vendor data / mail-protocol / mail-auth / agent substrate / Node.js CVE backstops.
|
|
13
14
|
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -301,6 +301,17 @@ function present(opts) {
|
|
|
301
301
|
// Hardcoded sha256 here previously diverged from the verifier when
|
|
302
302
|
// an issuer used a non-default hash, producing sd-hash-mismatch on
|
|
303
303
|
// valid presentations.
|
|
304
|
+
//
|
|
305
|
+
// Defense-in-depth: this pre-parse runs on the holder side
|
|
306
|
+
// (presentation builder) and reads `_sd_alg` from UNSIGNED bytes,
|
|
307
|
+
// because the holder needs to know which hash to use BEFORE the
|
|
308
|
+
// verifier sees the presentation. The presentation itself carries
|
|
309
|
+
// the JWS-signed issuer JWT verbatim; verify() re-parses the
|
|
310
|
+
// payload from the cryptographically-verified signing input. That
|
|
311
|
+
// post-verify decode is the source of truth — a holder who tampers
|
|
312
|
+
// with `_sd_alg` here only breaks their own KB-JWT digest, since
|
|
313
|
+
// the verifier recomputes from the signed bytes. No security
|
|
314
|
+
// boundary is crossed by reading the value here.
|
|
304
315
|
var _issuerPayload = null;
|
|
305
316
|
var _jwtParts = jwt.split(".");
|
|
306
317
|
if (_jwtParts.length === 3) {
|
package/lib/cli.js
CHANGED
|
@@ -48,6 +48,7 @@ var C = require("./constants");
|
|
|
48
48
|
var bCrypto = require("./crypto");
|
|
49
49
|
var dev = require("./dev");
|
|
50
50
|
var fileType = require("./file-type");
|
|
51
|
+
var guardRegex = require("./guard-regex");
|
|
51
52
|
var migrations = require("./migrations");
|
|
52
53
|
var passwordModule = require("./auth/password");
|
|
53
54
|
var requestHelpers = require("./request-helpers");
|
|
@@ -357,6 +358,18 @@ async function _runDev(args, ctx) {
|
|
|
357
358
|
"blamejs dev: --ignore pattern exceeds max length " +
|
|
358
359
|
MAX_IGNORE_PATTERN_LENGTH + " (got " + str.length + ")");
|
|
359
360
|
}
|
|
361
|
+
// ReDoS / catastrophic-backtracking defense — refuses nested-quant
|
|
362
|
+
// (CVE-2024-21538 class), consecutive-* (CVE-2026-26996), nested
|
|
363
|
+
// extglob (CVE-2026-33671), and lookaround-quant shapes before the
|
|
364
|
+
// pattern reaches RegExp(). Operator typo / hostile-input identical
|
|
365
|
+
// shape from here on — both want the same refusal.
|
|
366
|
+
try {
|
|
367
|
+
guardRegex.sanitize(str, { profile: "strict" });
|
|
368
|
+
} catch (e) {
|
|
369
|
+
throw new CliError("cli/bad-ignore-pattern",
|
|
370
|
+
"blamejs dev: --ignore pattern refused by guardRegex: " +
|
|
371
|
+
((e && e.message) || String(e)));
|
|
372
|
+
}
|
|
360
373
|
return RegExp(str);
|
|
361
374
|
});
|
|
362
375
|
var graceMs = args.flags["grace-ms"] !== undefined ? Number(args.flags["grace-ms"]) : undefined;
|
package/lib/guard-graphql.js
CHANGED
|
@@ -89,6 +89,15 @@ void observability;
|
|
|
89
89
|
|
|
90
90
|
var _err = GuardGraphqlError.factory;
|
|
91
91
|
|
|
92
|
+
// Query-body proto-poison literal (CVE-2026-32621). Matches the bare
|
|
93
|
+
// identifier in field / alias / variable-declaration positions —
|
|
94
|
+
// `$__proto__: String`, `__proto__: realField`, `__proto__ { ... }`,
|
|
95
|
+
// and the no-whitespace alias form `query { a:__proto__ }` /
|
|
96
|
+
// `query { a:constructor }` (GraphQL parsers accept the colon with
|
|
97
|
+
// or without trailing whitespace, so `:` is a valid identifier-
|
|
98
|
+
// position prefix that must also trigger refusal).
|
|
99
|
+
var PROTO_POISON_QUERY_RE = /[\s,({:]\$?(?:__proto__|constructor|prototype)\b/;
|
|
100
|
+
|
|
92
101
|
// ---- Profile presets ----
|
|
93
102
|
|
|
94
103
|
var PROFILES = Object.freeze({
|
|
@@ -319,6 +328,34 @@ function _detectIssues(req, opts) {
|
|
|
319
328
|
} catch (_e) { /* unstringifiable variables */ }
|
|
320
329
|
}
|
|
321
330
|
|
|
331
|
+
// Prototype-pollution defense (CVE-2026-32621). A `__proto__` /
|
|
332
|
+
// `constructor` / `prototype` variable key OR query-body identifier
|
|
333
|
+
// pivots a downstream deep-merge / deep-set into a poisoned shape.
|
|
334
|
+
// Refused at every profile.
|
|
335
|
+
var pVar = req.variables;
|
|
336
|
+
var pHas = Object.prototype.hasOwnProperty;
|
|
337
|
+
var pName = (pVar && typeof pVar === "object" && !Array.isArray(pVar) &&
|
|
338
|
+
(pHas.call(pVar, "__proto__") ? "__proto__" :
|
|
339
|
+
pHas.call(pVar, "constructor") ? "constructor" :
|
|
340
|
+
pHas.call(pVar, "prototype") ? "prototype" : null));
|
|
341
|
+
if (pName) {
|
|
342
|
+
issues.push({
|
|
343
|
+
kind: "variable-prototype-poison", severity: "critical",
|
|
344
|
+
ruleId: "graphql.variable-prototype-poison",
|
|
345
|
+
snippet: "variable name `" + pName + "` — prototype-pollution " +
|
|
346
|
+
"gadget (CVE-2026-32621)",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (PROTO_POISON_QUERY_RE.test(req.query)) { // allow:regex-no-length-cap — input bounded by maxQueryBytes above
|
|
350
|
+
issues.push({
|
|
351
|
+
kind: "query-prototype-poison", severity: "critical",
|
|
352
|
+
ruleId: "graphql.query-prototype-poison",
|
|
353
|
+
snippet: "query references `__proto__` / `constructor` / " +
|
|
354
|
+
"`prototype` as a field / alias / variable — prototype-" +
|
|
355
|
+
"pollution gadget (CVE-2026-32621)",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
322
359
|
// Introspection.
|
|
323
360
|
if (opts.introspectionPolicy !== "allow") {
|
|
324
361
|
if (req.query.indexOf("__schema") !== -1 ||
|
package/lib/guard-regex.js
CHANGED
|
@@ -70,6 +70,14 @@ var BOUNDED_REPEAT_RE = /\{(\d+)(?:,(\d*))?\}/g;
|
|
|
70
70
|
// Lookaround with internal quantifier — `(?=.*+)`, `(?!a*)`.
|
|
71
71
|
var LOOKAROUND_QUANT_RE = /\(\?[=!<][^()]*[*+]/;
|
|
72
72
|
|
|
73
|
+
// Nested extglob detector — picomatch `*(...)` / `+(...)` / `?(...)` /
|
|
74
|
+
// `@(...)` / `!(...)` containing another extglob inside (CVE-2026-33671
|
|
75
|
+
// nested-extglob catastrophic-backtracking class). Two extglob heads in
|
|
76
|
+
// the same pattern with no closing paren between them indicates nesting.
|
|
77
|
+
// The consecutive-star detector (CVE-2026-26996) walks the input by
|
|
78
|
+
// char so doesn't need a regex literal.
|
|
79
|
+
var EXTGLOB_HEAD_RE = /[*+?@!]\(/g; // allow:regex-no-length-cap — input bounded by maxPatternBytes
|
|
80
|
+
|
|
73
81
|
// ---- Profile presets ----
|
|
74
82
|
|
|
75
83
|
var PROFILES = Object.freeze({
|
|
@@ -82,7 +90,11 @@ var PROFILES = Object.freeze({
|
|
|
82
90
|
alternationQuantPolicy: "reject",
|
|
83
91
|
boundedRepeatPolicy: "reject",
|
|
84
92
|
lookaroundQuantPolicy: "reject",
|
|
93
|
+
consecutiveStarPolicy: "reject",
|
|
94
|
+
nestedExtglobPolicy: "reject",
|
|
95
|
+
inputKind: "regex", // CVE-2026-26996 + CVE-2026-33671 detectors apply only when inputKind=="glob"
|
|
85
96
|
maxBoundedRepeat: 100, // allow:raw-byte-literal — bounded repeat ceiling
|
|
97
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
86
98
|
maxPatternBytes: C.BYTES.kib(1),
|
|
87
99
|
maxBytes: C.BYTES.kib(1),
|
|
88
100
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -96,7 +108,10 @@ var PROFILES = Object.freeze({
|
|
|
96
108
|
alternationQuantPolicy: "audit",
|
|
97
109
|
boundedRepeatPolicy: "audit",
|
|
98
110
|
lookaroundQuantPolicy: "audit",
|
|
111
|
+
consecutiveStarPolicy: "reject", // CVE-2026-26996 refused at every profile
|
|
112
|
+
nestedExtglobPolicy: "reject", // CVE-2026-33671 refused at every profile
|
|
99
113
|
maxBoundedRepeat: 1000, // allow:raw-byte-literal — bounded repeat ceiling
|
|
114
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
100
115
|
maxPatternBytes: C.BYTES.kib(2),
|
|
101
116
|
maxBytes: C.BYTES.kib(2),
|
|
102
117
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -110,7 +125,10 @@ var PROFILES = Object.freeze({
|
|
|
110
125
|
alternationQuantPolicy: "allow",
|
|
111
126
|
boundedRepeatPolicy: "audit",
|
|
112
127
|
lookaroundQuantPolicy: "audit",
|
|
128
|
+
consecutiveStarPolicy: "reject", // CVE-2026-26996 refused at every profile
|
|
129
|
+
nestedExtglobPolicy: "reject", // CVE-2026-33671 refused at every profile
|
|
113
130
|
maxBoundedRepeat: 10000, // allow:raw-byte-literal — bounded repeat ceiling
|
|
131
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
114
132
|
maxPatternBytes: C.BYTES.kib(8),
|
|
115
133
|
maxBytes: C.BYTES.kib(8),
|
|
116
134
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -223,9 +241,116 @@ function _detectIssues(input, opts) {
|
|
|
223
241
|
}
|
|
224
242
|
}
|
|
225
243
|
|
|
244
|
+
_detectConsecutiveStar(input, opts, issues);
|
|
245
|
+
_detectNestedExtglob(input, opts, issues);
|
|
246
|
+
|
|
226
247
|
return issues;
|
|
227
248
|
}
|
|
228
249
|
|
|
250
|
+
// Consecutive-star wildcard cap (CVE-2026-26996). Operator-supplied
|
|
251
|
+
// glob fragments compile to picomatch / RegExp; a long run of `*`
|
|
252
|
+
// against a non-matching literal walks O(4^N). Three-or-more
|
|
253
|
+
// consecutive `*` is the canonical bad shape; `**` (recursive glob)
|
|
254
|
+
// stays permitted, gated by the profile's `maxConsecutiveStars`.
|
|
255
|
+
function _detectConsecutiveStar(input, opts, issues) {
|
|
256
|
+
if (opts.consecutiveStarPolicy === "allow") return;
|
|
257
|
+
// CVE-2026-26996 is a picomatch / glob-shape backtracking class —
|
|
258
|
+
// `***+literal` walks O(4^N) when picomatch translates the run to a
|
|
259
|
+
// backtracking-heavy regex. Native ECMAScript regex syntax cannot
|
|
260
|
+
// produce three consecutive `*` quantifiers (it's a SyntaxError),
|
|
261
|
+
// so applying this detector to `inputKind: "regex"` strings only
|
|
262
|
+
// produces false positives on legitimate regex shapes like
|
|
263
|
+
// `a*(b)*` where `*(` is quantifier+group, not extglob.
|
|
264
|
+
if (opts.inputKind !== "glob") return;
|
|
265
|
+
var starRun = 0;
|
|
266
|
+
var starRunMax = 0;
|
|
267
|
+
for (var si = 0; si < input.length; si += 1) {
|
|
268
|
+
if (input.charAt(si) === "*") {
|
|
269
|
+
starRun += 1;
|
|
270
|
+
if (starRun > starRunMax) starRunMax = starRun;
|
|
271
|
+
} else {
|
|
272
|
+
starRun = 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var starCeiling = opts.maxConsecutiveStars === undefined ?
|
|
276
|
+
2 : opts.maxConsecutiveStars; // allow:raw-byte-literal — `**` glob ceiling
|
|
277
|
+
if (starRunMax > starCeiling) {
|
|
278
|
+
issues.push({
|
|
279
|
+
kind: "consecutive-star",
|
|
280
|
+
severity: opts.consecutiveStarPolicy === "reject" ? "critical" : "high",
|
|
281
|
+
ruleId: "regex.consecutive-star",
|
|
282
|
+
snippet: "pattern has " + starRunMax + " consecutive `*` " +
|
|
283
|
+
"wildcards (cap " + starCeiling + ") — O(4^N) " +
|
|
284
|
+
"backtracking on non-matching literal (CVE-2026-26996)",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Nested-extglob detector (CVE-2026-33671). picomatch `*(...)` /
|
|
290
|
+
// `+(...)` / `?(...)` / `@(...)` / `!(...)` containing another
|
|
291
|
+
// extglob inside compiles to catastrophic-backtracking regex.
|
|
292
|
+
function _detectNestedExtglob(input, opts, issues) {
|
|
293
|
+
if (opts.nestedExtglobPolicy === "allow") return;
|
|
294
|
+
// CVE-2026-33671 is picomatch-specific: the extglob heads `*(`/
|
|
295
|
+
// `+(`/`?(`/`@(`/`!(` collide with valid ECMAScript regex shapes
|
|
296
|
+
// (quantifier + capturing group). Restricting this detector to
|
|
297
|
+
// `inputKind: "glob"` avoids false-positive refusal of regex
|
|
298
|
+
// patterns like `a*(b+(c))` where the heads are quantifier
|
|
299
|
+
// groupings, not extglob.
|
|
300
|
+
if (opts.inputKind !== "glob") return;
|
|
301
|
+
// Collect extglob head positions via match() — read-only scan.
|
|
302
|
+
var heads = [];
|
|
303
|
+
var allHeads = input.match(EXTGLOB_HEAD_RE); // allow:regex-no-length-cap — input bounded by maxPatternBytes
|
|
304
|
+
if (allHeads === null || allHeads.length < 2) return;
|
|
305
|
+
// Locate each head index manually (match returns substrings, not idx).
|
|
306
|
+
var scanFrom = 0;
|
|
307
|
+
for (var hh = 0; hh < allHeads.length; hh += 1) {
|
|
308
|
+
var ch0 = allHeads[hh].charAt(0);
|
|
309
|
+
var idx = scanFrom;
|
|
310
|
+
while (idx < input.length - 1) {
|
|
311
|
+
var c0 = input.charAt(idx);
|
|
312
|
+
var c1 = input.charAt(idx + 1);
|
|
313
|
+
if (c1 === "(" && c0 === ch0) break;
|
|
314
|
+
idx += 1;
|
|
315
|
+
}
|
|
316
|
+
heads.push(idx);
|
|
317
|
+
scanFrom = idx + 1;
|
|
318
|
+
if (heads.length > 1024) break; // allow:raw-byte-literal — head-count safety cap
|
|
319
|
+
}
|
|
320
|
+
var nested = false;
|
|
321
|
+
for (var hi = 0; hi < heads.length && !nested; hi += 1) {
|
|
322
|
+
var headStart = heads[hi];
|
|
323
|
+
// Walk forward tracking paren depth. Inner head before close = nested.
|
|
324
|
+
var pdepth = 1;
|
|
325
|
+
for (var pj = headStart + 2; pj < input.length && pdepth > 0; pj += 1) {
|
|
326
|
+
var ch = input.charAt(pj);
|
|
327
|
+
if (ch === "(") {
|
|
328
|
+
pdepth += 1;
|
|
329
|
+
if (pj > 0) {
|
|
330
|
+
var preVerb = input.charAt(pj - 1);
|
|
331
|
+
if (preVerb === "*" || preVerb === "+" || preVerb === "?" ||
|
|
332
|
+
preVerb === "@" || preVerb === "!") {
|
|
333
|
+
nested = true;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (ch === ")") {
|
|
338
|
+
pdepth -= 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (nested) {
|
|
343
|
+
issues.push({
|
|
344
|
+
kind: "nested-extglob",
|
|
345
|
+
severity: opts.nestedExtglobPolicy === "reject" ? "critical" : "high",
|
|
346
|
+
ruleId: "regex.nested-extglob",
|
|
347
|
+
snippet: "pattern contains nested extglob quantifier " +
|
|
348
|
+
"(`*(...*(...))`) — catastrophic backtracking class " +
|
|
349
|
+
"(CVE-2026-33671 picomatch)",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
229
354
|
/**
|
|
230
355
|
* @primitive b.guardRegex.validate
|
|
231
356
|
* @signature b.guardRegex.validate(input, opts)
|
|
@@ -252,7 +377,11 @@ function _detectIssues(input, opts) {
|
|
|
252
377
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
253
378
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
254
379
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
380
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
381
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
382
|
+
* inputKind: "regex"|"glob",
|
|
255
383
|
* maxBoundedRepeat: number,
|
|
384
|
+
* maxConsecutiveStars: number,
|
|
256
385
|
* maxPatternBytes: number,
|
|
257
386
|
* maxBytes: number,
|
|
258
387
|
* maxRuntimeMs: number,
|
|
@@ -268,7 +397,7 @@ function _detectIssues(input, opts) {
|
|
|
268
397
|
function validate(input, opts) {
|
|
269
398
|
opts = _resolveOpts(opts);
|
|
270
399
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
271
|
-
["maxBytes", "maxPatternBytes", "maxBoundedRepeat"],
|
|
400
|
+
["maxBytes", "maxPatternBytes", "maxBoundedRepeat", "maxConsecutiveStars"],
|
|
272
401
|
"guardRegex.validate", GuardRegexError, "regex.bad-opt");
|
|
273
402
|
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
274
403
|
}
|
|
@@ -298,7 +427,11 @@ function validate(input, opts) {
|
|
|
298
427
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
299
428
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
300
429
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
430
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
431
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
432
|
+
* inputKind: "regex"|"glob",
|
|
301
433
|
* maxBoundedRepeat: number,
|
|
434
|
+
* maxConsecutiveStars: number,
|
|
302
435
|
* maxPatternBytes: number,
|
|
303
436
|
*
|
|
304
437
|
* @example
|
|
@@ -350,7 +483,11 @@ function sanitize(input, opts) {
|
|
|
350
483
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
351
484
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
352
485
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
486
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
487
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
488
|
+
* inputKind: "regex"|"glob",
|
|
353
489
|
* maxBoundedRepeat: number,
|
|
490
|
+
* maxConsecutiveStars: number,
|
|
354
491
|
* maxPatternBytes: number,
|
|
355
492
|
*
|
|
356
493
|
* @example
|
package/lib/guard-xml.js
CHANGED
|
@@ -91,6 +91,16 @@ var PROCESSING_INSTR_RE = /<\?[A-Za-z][\w:-]*/;
|
|
|
91
91
|
var CDATA_RE = /<!\[CDATA\[/;
|
|
92
92
|
var XMLDSIG_RE = /<\w*:?Signature\b[^>]*xmldsig/i;
|
|
93
93
|
|
|
94
|
+
// Numeric character reference (NCR) detector. Per XML 1.0 §4.1 every
|
|
95
|
+
// `&#<digits>;` / `&#x<hex>;` is a character reference; a hostile input
|
|
96
|
+
// fanning these out in the hundreds of thousands bypasses entity-
|
|
97
|
+
// expansion caps that count only `&name;` general entities (CVE-2026-
|
|
98
|
+
// 26278 / CVE-2026-33036 .NET XmlReader class). Per-document NCR count
|
|
99
|
+
// is gated by `maxNumericCharRefs` independent of the entity-policy
|
|
100
|
+
// branch so the operator can't disable the cap by setting
|
|
101
|
+
// `entityPolicy: "allow"` for a downstream signed-XML case.
|
|
102
|
+
var NUMERIC_CHAR_REF_RE = /&#(?:[0-9]+|x[0-9a-fA-F]+);/g; // allow:regex-no-length-cap — input bounded by maxBytes above
|
|
103
|
+
|
|
94
104
|
// ---- Profile presets ----
|
|
95
105
|
|
|
96
106
|
var PROFILES = Object.freeze({
|
|
@@ -112,6 +122,7 @@ var PROFILES = Object.freeze({
|
|
|
112
122
|
maxElements: 8192, // allow:raw-byte-literal — element count cap, not byte size
|
|
113
123
|
maxAttrsPerElement: 64, // allow:raw-byte-literal — attr count, not byte size
|
|
114
124
|
maxAttrValueBytes: C.BYTES.kib(8),
|
|
125
|
+
maxNumericCharRefs: 1024, // allow:raw-byte-literal — NCR fan-out cap (CVE-2026-26278)
|
|
115
126
|
},
|
|
116
127
|
"balanced": {
|
|
117
128
|
doctypePolicy: "reject", // DOCTYPE is XXE vector regardless
|
|
@@ -131,6 +142,7 @@ var PROFILES = Object.freeze({
|
|
|
131
142
|
maxElements: 65536, // allow:raw-byte-literal — element count cap, not byte size
|
|
132
143
|
maxAttrsPerElement: 128, // allow:raw-byte-literal — attr count, not byte size
|
|
133
144
|
maxAttrValueBytes: C.BYTES.kib(32),
|
|
145
|
+
maxNumericCharRefs: 16384, // allow:raw-byte-literal — NCR fan-out cap (CVE-2026-26278)
|
|
134
146
|
},
|
|
135
147
|
"permissive": {
|
|
136
148
|
doctypePolicy: "reject", // billion-laughs class always
|
|
@@ -150,6 +162,7 @@ var PROFILES = Object.freeze({
|
|
|
150
162
|
maxElements: 262144, // allow:raw-byte-literal — element count cap, not byte size
|
|
151
163
|
maxAttrsPerElement: 256, // allow:raw-byte-literal — attr count, not byte size
|
|
152
164
|
maxAttrValueBytes: C.BYTES.kib(64),
|
|
165
|
+
maxNumericCharRefs: 262144, // allow:raw-byte-literal — NCR fan-out cap (CVE-2026-26278)
|
|
153
166
|
},
|
|
154
167
|
});
|
|
155
168
|
|
|
@@ -282,6 +295,30 @@ function _detectIssues(input, opts) {
|
|
|
282
295
|
});
|
|
283
296
|
}
|
|
284
297
|
|
|
298
|
+
// 8a. Numeric character reference fan-out — `&#NNNN;` / `&#xHHHH;`.
|
|
299
|
+
// Bypasses the `<!ENTITY>`-counting expansion caps because NCRs are
|
|
300
|
+
// parser-resolved, not document-level entities (CVE-2026-26278 /
|
|
301
|
+
// CVE-2026-33036 .NET XmlReader class). Counted regardless of
|
|
302
|
+
// entityPolicy so signed-XML paths that need entities-allowed don't
|
|
303
|
+
// get the NCR cap disabled with them. The `maxNumericCharRefs` opt
|
|
304
|
+
// is validated by `numericBounds.requireAllPositiveFiniteIntIfPresent`
|
|
305
|
+
// at the public-surface boundary above.
|
|
306
|
+
var ncrCap = opts.maxNumericCharRefs; // allow:numeric-opt-no-bounds-check — validated at public boundary
|
|
307
|
+
if (ncrCap !== undefined && ncrCap !== null) {
|
|
308
|
+
var ncrMatches = input.match(NUMERIC_CHAR_REF_RE); // allow:regex-no-length-cap — input bounded by maxBytes above
|
|
309
|
+
var ncrCount = ncrMatches === null ? 0 : ncrMatches.length;
|
|
310
|
+
if (ncrCount > ncrCap) {
|
|
311
|
+
issues.push({
|
|
312
|
+
kind: "numeric-char-ref-cap", severity: "critical",
|
|
313
|
+
ruleId: "xml.numeric-char-ref-cap",
|
|
314
|
+
snippet: "numeric character reference count " + ncrCount +
|
|
315
|
+
" exceeds maxNumericCharRefs " + ncrCap +
|
|
316
|
+
" — NCR fan-out bypasses entity-expansion caps " +
|
|
317
|
+
"(CVE-2026-26278 / CVE-2026-33036)",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
285
322
|
// 9. Codepoint-class threats.
|
|
286
323
|
issues.push.apply(issues, codepointClass.detectCharThreats(input, opts, "xml"));
|
|
287
324
|
|
|
@@ -369,6 +406,7 @@ function _detectIssues(input, opts) {
|
|
|
369
406
|
* maxElements: number, // total open-tag count cap
|
|
370
407
|
* maxAttrsPerElement: number, // attribute count cap per element
|
|
371
408
|
* maxAttrValueBytes: number, // per-attr-value length cap
|
|
409
|
+
* maxNumericCharRefs: number, // numeric character reference cap
|
|
372
410
|
*
|
|
373
411
|
* @example
|
|
374
412
|
* var hostile = '<?xml version="1.0"?>\n' +
|
|
@@ -381,7 +419,7 @@ function validate(input, opts) {
|
|
|
381
419
|
opts = _resolveOpts(opts);
|
|
382
420
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
383
421
|
["maxBytes", "maxDepth", "maxElements", "maxAttrsPerElement",
|
|
384
|
-
"maxAttrValueBytes"],
|
|
422
|
+
"maxAttrValueBytes", "maxNumericCharRefs"],
|
|
385
423
|
"guardXml.validate", GuardXmlError, "xml.bad-opt");
|
|
386
424
|
if (typeof input !== "string") {
|
|
387
425
|
return {
|
package/lib/otel-export.js
CHANGED
|
@@ -50,6 +50,13 @@ var OtelExportError = defineClass("OtelExportError", { alwaysPermanent: false })
|
|
|
50
50
|
|
|
51
51
|
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
52
52
|
|
|
53
|
+
// OTLP collector response is `Empty` (zero protobuf bytes) on success
|
|
54
|
+
// or a short ExportPartialSuccess message on partial accept. A response
|
|
55
|
+
// past this cap is a hostile / misbehaving collector; refusing the
|
|
56
|
+
// body keeps the exporter from buffering megabytes per flush (CVE-2026-
|
|
57
|
+
// 40891 / CVE-2026-40182 OTLP class).
|
|
58
|
+
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
59
|
+
|
|
53
60
|
// OTLP aggregation temporality:
|
|
54
61
|
// 1 = DELTA — counters report deltas since last export
|
|
55
62
|
// 2 = CUMULATIVE — counters report running totals
|
|
@@ -221,10 +228,12 @@ function create(opts) {
|
|
|
221
228
|
var body = JSON.stringify(payload);
|
|
222
229
|
try {
|
|
223
230
|
var res = await effectiveHttpClient.request({
|
|
224
|
-
method:
|
|
225
|
-
url:
|
|
226
|
-
headers:
|
|
227
|
-
body:
|
|
231
|
+
method: "POST",
|
|
232
|
+
url: endpoint,
|
|
233
|
+
headers: Object.assign({ "Content-Type": "application/json" }, headers),
|
|
234
|
+
body: body,
|
|
235
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
236
|
+
errorClass: OtelExportError,
|
|
228
237
|
});
|
|
229
238
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
230
239
|
throw new OtelExportError("otel-export/upstream-rejected",
|
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.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:c1165275-044f-4a5a-b7dc-061727bfd076",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-16T23:28:39.659Z",
|
|
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.10.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.10.2",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.10.
|
|
25
|
+
"version": "0.10.2",
|
|
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.10.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.10.2",
|
|
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.10.
|
|
57
|
+
"ref": "@blamejs/core@0.10.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|