@blamejs/core 0.13.8 → 0.13.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.10 (2026-05-27) — **Documented-but-inert options wired up, a non-existent CVE reference removed, and a silent iCalendar cap-bypass fixed.** A sweep for places where a documented option or citation did not match what the code does. The most operator-relevant fix: b.calendar.fromIcal documented a safeIcalOpts option that forwards parser caps (byte size, RRULE limits, nesting depth) to b.safeIcal.parse, but the value was never forwarded — so an operator who set tight caps through it got the default profile instead, silently. That is corrected; the nested options now reach the parser. b.archive.read.zip documented an AbortSignal option that was never honored; it now aborts the read at the entry boundary. b.auth.fal documented a bearerOnly alias that had no effect; it now forces the no-proof-of-possession path and refuses the contradictory combination of bearerOnly:true with a holder-of-key binding. Separately, the auth verification paths cited CVE-2026-23993 (13 places) for the "reject an unknown alg before key lookup" guard — that CVE id does not exist (the registry has no record of it); the citation is replaced with the weakness class (CWE-347 / CWE-757) and the real, verifiable neighboring CVEs. The circuit-breaker error-code note that promised a rename "in v0.10" is corrected to the actual plan (v1.0), and the build gate that catches overdue version promises now also catches two-part version numbers. **Changed:** *`b.auth.fal` `bearerOnly` is now a real alias and refuses contradictions* — `bearerOnly: true` now forces the no-proof-of-possession path (equivalent to `hokBinding: null`), as documented. Passing `bearerOnly: true` together with a non-null `hokBinding` is a contradictory assurance request and is now refused at the call rather than silently resolved one way. **Fixed:** *`b.calendar.fromIcal` now forwards `safeIcalOpts` to the parser* — The documented `safeIcalOpts` option (parser caps: max bytes, RRULE COUNT/BYxxx limits, nesting depth) was not being passed to `b.safeIcal.parse` — when supplied under the documented nested key it was silently ignored and the parser ran with its default profile. Both forms now reach the parser: the documented nested `{ safeIcalOpts: { ... } }` and the top-level `{ profile, ... }` that earlier releases accepted, with the nested form winning on conflict. No caller regresses. · *`b.archive.read.zip` honors the documented `signal` (AbortSignal)* — The `signal` option was documented but never read. A large or slow archive read can now be aborted cooperatively — the reader checks the signal at each entry boundary (`inspect`, `entries`, `extractEntries`, `extract`) and rejects with an `archive-read/aborted` error. · *Removed a non-existent CVE reference from the JWT/JWE verification paths* — The "reject an unknown/unsupported `alg` before any key lookup" guard in `b.auth.jwt.verifyExternal`, `b.auth.oauth.verifyIdToken`, `b.auth.oid4vci`, and `b.auth.sd-jwt-vc` cited a CVE id that the registry has no record of. The behaviour is unchanged; the citation is now the weakness class it defends (CWE-347 improper signature verification / CWE-757 algorithm downgrade) alongside the real, verifiable alg-confusion / JWE-bypass CVEs already cited beside it. **Detectors:** *Overdue-version-promise gate now catches two-part version numbers* — The build gate that flags a deferral whose promised landing version has already shipped previously matched only three-part versions (`vN.N.N`); a two-part promise (`vN.N`) slipped past it. It now matches both. The `b.circuitBreaker` `CIRCUIT_OPEN` error-code note that pointed at a passed version is corrected to its actual plan (rename at v1.0, with a deprecation warning a minor ahead).
12
+
13
+ - v0.13.9 (2026-05-26) — **Corrected CVE citations in source threat annotations + a build gate that refuses malformed CVE identifiers.** Several source-comment threat annotations cited CVE identifiers that were rejected by the numbering authority (never assigned to a real issue), attributed to the wrong product, or structurally malformed (a placeholder with a non-numeric sequence). The annotated defenses are unchanged — every cap, refusal, and constant-time comparison behaves exactly as before; only the reference labels were corrected, each to a verifiable CVE or to the underlying weakness class (CWE / RFC) where no single CVE fits. Notable corrections: the S/MIME SHA-1 / MD5 certificate-signature refusal now cites the SHAttered collision and RFC 8551 §2.5 instead of a rejected candidate id; decompression-output caps cite CWE-409 and CVE-2025-0725 instead of a fabricated placeholder; the iCalendar RRULE / nesting / byte caps describe the calendar-bomb recursion-DoS class instead of an unrelated SSRF advisory; and the SAML signature-wrapping (XSW) defense now cites the actively-exploited CVE-2024-45409 (ruby-saml, CVSS 10.0) and CVE-2025-25291 / -25292 that the duplicate-element refusal defeats. A new build-time detector refuses any CVE token whose sequence number is not all-numeric, so a placeholder identifier can never reach a release again. **Fixed:** *Corrected rejected / misattributed / malformed CVE references in source threat annotations* — Threat-annotation comments across the mail, crypto, auth, guard, and safe modules carried CVE identifiers that were rejected by the CVE numbering authority, attributed to the wrong product, or written as non-numeric placeholders. Each was corrected to a verifiable CVE or to the weakness class (CWE / RFC) it defends. No runtime behaviour changed — the defenses these comments describe are unchanged. The S/MIME certificate check's SHA-1 / MD5 refusal message now names the SHAttered collision and RFC 8551 §2.5; the SAML XSW defense now names CVE-2024-45409 and CVE-2025-25291 / -25292. **Detectors:** *`malformed-cve-identifier` — refuses structurally-invalid CVE tokens at build time* — A CVE identifier's sequence number is always numeric (`CVE-<year>-<digits>`). The new detector refuses any CVE token whose post-year segment contains a letter — the placeholder shape that lets a fabricated reference slip past review. It cannot verify that a well-formed id is real or correctly attributed (that stays a review responsibility), but it makes the structurally-invalid class impossible to ship.
14
+
11
15
  - v0.13.8 (2026-05-26) — **In-memory archive extraction for read-only / serverless filesystems.** Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead. **Added:** *`extractEntries()` — in-memory archive extraction (ZIP + tar)* — `b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes. **Fixed:** *Removed the inaccurate `b.archive.zip().toGzip()` doc claim* — The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged.
12
16
 
13
17
  - v0.13.7 (2026-05-26) — **Documentation accuracy — several primitives described shipped features as deferred.** A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped. **Fixed:** *Corrected `deferred`/`does-not-ship` docs for features that have shipped* — `b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such. **Detectors:** *Overdue-defer detector in the codebase-pattern gate* — A new check fails the build when a comment promises a feature "lands in" / is "deferred to" / is "not supported in" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions.
@@ -503,6 +503,18 @@ function zip(adapter, opts) {
503
503
  var entryTypePolicy = _normalizeEntryTypePolicy(opts.entryTypePolicy);
504
504
  var cdCache = null;
505
505
 
506
+ // Cooperative cancellation — operators pass an AbortSignal to bound a
507
+ // large/slow archive read. Checked between entries (the natural yield
508
+ // point); a long single-entry decompress is already bounded by the
509
+ // bomb policy.
510
+ function _throwIfAborted() {
511
+ if (opts.signal && opts.signal.aborted) {
512
+ throw new ArchiveReadError("archive-read/aborted",
513
+ "archive read aborted via opts.signal" +
514
+ (opts.signal.reason !== undefined ? ": " + String(opts.signal.reason) : ""));
515
+ }
516
+ }
517
+
506
518
  async function _loadCD() {
507
519
  if (cdCache) return cdCache;
508
520
  var eocd = await _locateEocd(adapter);
@@ -512,6 +524,7 @@ function zip(adapter, opts) {
512
524
  }
513
525
 
514
526
  async function inspect() {
527
+ _throwIfAborted();
515
528
  var loaded = await _loadCD();
516
529
  _enforceBombPolicy(loaded.entries, bombPolicy);
517
530
  _emitAudit(opts, "archive.read.inspect", "success", {
@@ -539,9 +552,11 @@ function zip(adapter, opts) {
539
552
  }
540
553
 
541
554
  async function* entries() {
555
+ _throwIfAborted();
542
556
  var loaded = await _loadCD();
543
557
  _enforceBombPolicy(loaded.entries, bombPolicy);
544
558
  for (var i = 0; i < loaded.entries.length; i += 1) {
559
+ _throwIfAborted();
545
560
  yield loaded.entries[i];
546
561
  }
547
562
  }
@@ -593,6 +608,7 @@ function zip(adapter, opts) {
593
608
  var totalDecompressed = 0;
594
609
  var yielded = 0;
595
610
  for (var i = 0; i < loaded.entries.length; i += 1) {
611
+ _throwIfAborted();
596
612
  var entry = loaded.entries[i];
597
613
  if (entry.isEncrypted && !extractOpts.allowEncrypted) {
598
614
  throw new ArchiveReadError("archive-read/encrypted-entry",
@@ -644,6 +660,7 @@ function zip(adapter, opts) {
644
660
  var totalDecompressed = 0;
645
661
  try {
646
662
  for (var i = 0; i < loaded.entries.length; i += 1) {
663
+ _throwIfAborted();
647
664
  var entry = loaded.entries[i];
648
665
  // Skip directory + dangerous-by-default entry types unless the
649
666
  // entry-type policy opts in.
package/lib/auth/fal.js CHANGED
@@ -155,6 +155,18 @@ function fromAssertion(opts) {
155
155
  "fal.fromAssertion: channel must be 'front' or 'back'");
156
156
  }
157
157
  var hokBinding = opts.hokBinding;
158
+ // `bearerOnly: true` is the explicit alias for "no proof-of-possession
159
+ // binding" (hokBinding === null). It contradicts a non-null hokBinding;
160
+ // refuse the contradiction at the entry point rather than silently
161
+ // picking one — an operator who sets both has a config bug.
162
+ if (opts.bearerOnly === true) {
163
+ if (hokBinding !== undefined && hokBinding !== null) {
164
+ throw new AuthError("auth/bad-fal-opts",
165
+ "fal.fromAssertion: bearerOnly:true conflicts with hokBinding '" + hokBinding +
166
+ "' (bearerOnly forces no proof-of-possession binding)");
167
+ }
168
+ hokBinding = null;
169
+ }
158
170
  if (hokBinding !== undefined && hokBinding !== null) {
159
171
  if (hokBinding !== "mtls" && hokBinding !== "dpop" && hokBinding !== "saml-hok") {
160
172
  throw new AuthError("auth/bad-fal-opts",
@@ -28,8 +28,8 @@
28
28
  *
29
29
  * Defenses against the well-known JWT pitfalls:
30
30
  *
31
- * - alg confusion (CVE-2024-54150 / CVE-2025-30144 / CVE-2026-22817
32
- * Hono class) — `algorithms` is REQUIRED with no default; `none`,
31
+ * - alg confusion (CVE-2024-54150 / CVE-2026-22817 Hono class) —
32
+ * `algorithms` is REQUIRED with no default; `none`,
33
33
  * `HS256` cannot be accepted unless the operator explicitly listed
34
34
  * them, and even then the verifier refuses HS* algs in
35
35
  * verifyExternal because HMAC + a public-key JWKS is the canonical
@@ -175,8 +175,8 @@ function _assertAlgKtyMatch(alg, jwk) {
175
175
  else if (alg === "ML-DSA-65" || alg === "ML-DSA-87") { expectedKty = "AKP"; }
176
176
  else {
177
177
  // Unknown alg — caller's alg allowlist should have rejected first;
178
- // refuse here defensively (CVE-2026-23993 class — unknown-alg paths
179
- // that skip downstream verification).
178
+ // refuse here defensively (CWE-347 alg-confusion class — unknown-alg
179
+ // paths that skip downstream verification; cf. CVE-2026-22817).
180
180
  throw new AuthError("auth-jwt-external/unsupported-alg",
181
181
  "_assertAlgKtyMatch: alg '" + alg + "' has no defined key-type binding");
182
182
  }
@@ -336,7 +336,7 @@ async function verifyExternal(token, opts) {
336
336
 
337
337
  // Decode header + payload.
338
338
  var parts = token.split(".");
339
- // CVE-2026-29000 / CVE-2026-23993 / CVE-2026-22817 / CVE-2026-34950 —
339
+ // CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 —
340
340
  // JWE-bypass + alg-confusion. A 5-segment compact serialization is a
341
341
  // JWE (RFC 7516); accepting it on a JWS verifier is the canonical
342
342
  // confused-deputy shape. verifyExternal is JWS-only; refuse JWE
@@ -350,7 +350,7 @@ async function verifyExternal(token, opts) {
350
350
  }); } catch (_e) { /* audit best-effort */ }
351
351
  throw new AuthError("auth-jwt-external/jwe-refused",
352
352
  "5-segment JWE token refused — verifyExternal only handles JWS " +
353
- "(JWE bypass class — CVE-2026-29000 / CVE-2026-23993 / CVE-2026-22817 / CVE-2026-34950)");
353
+ "(JWE bypass class — CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950)");
354
354
  }
355
355
  if (parts.length !== 3) {
356
356
  throw new AuthError("auth-jwt-external/malformed-jwt",
@@ -371,8 +371,9 @@ async function verifyExternal(token, opts) {
371
371
  throw new AuthError("auth-jwt-external/unknown-crit",
372
372
  "token declares 'crit' header — verifyExternal does not support critical extensions");
373
373
  }
374
- // CVE-2026-23993 refuse alg values outside the accepted list BEFORE
375
- // any key lookup. The early refusal closes the class where an
374
+ // Alg-allowlist gate (CWE-347 improper-sig-verification / CWE-757
375
+ // algorithm-downgrade) refuse alg values outside the accepted list
376
+ // BEFORE any key lookup. The early refusal closes the class where an
376
377
  // unknown / unsupported alg slips through to a downstream code path
377
378
  // that interprets it permissively. The per-listed algorithm check
378
379
  // above in the opts-validation loop refuses the OPERATOR'S allowlist
@@ -381,11 +382,11 @@ async function verifyExternal(token, opts) {
381
382
  if (opts.algorithms.indexOf(header.alg) === -1) {
382
383
  throw new AuthError("auth-jwt-external/alg-not-allowed",
383
384
  "token alg='" + header.alg + "' not in allowed list [" + opts.algorithms.join(", ") +
384
- "] (CVE-2026-23993 — refused before key lookup)");
385
+ "] (alg-allowlist gate — refused before key lookup)");
385
386
  }
386
387
  if (SUPPORTED_CLASSICAL_ALGS.indexOf(header.alg) === -1) {
387
388
  throw new AuthError("auth-jwt-external/unsupported-alg",
388
- "token alg='" + header.alg + "' is not in the verifier's supported set (CVE-2026-23993)");
389
+ "token alg='" + header.alg + "' is not in the verifier's supported set (alg-allowlist gate)");
389
390
  }
390
391
 
391
392
  // Resolve key.
@@ -480,7 +481,10 @@ async function verifyExternal(token, opts) {
480
481
  // weak iss validation. Constant-time compare defeats prefix-timing
481
482
  // narrowing; emit a DISTINCT audit event (separate from sig-verify-
482
483
  // fail) so detection signals lights up on the cross-realm shape
483
- // independently of generic verification failures.
484
+ // independently of generic verification failures. The `typeof ... !==
485
+ // "string"` guard also rejects an array-valued iss (CVE-2025-30144,
486
+ // fast-jwt — an iss array `["attacker", "valid"]` passed an any-match
487
+ // check); only a single string iss is accepted.
484
488
  if (typeof payload.iss !== "string" ||
485
489
  !_issuerMatches(payload.iss, opts.issuer)) {
486
490
  try { audit().safeEmit({
package/lib/auth/jwt.js CHANGED
@@ -234,8 +234,8 @@ async function verify(token, opts) {
234
234
  // SECURITY: when the resolver uses header.kid as a filename / map
235
235
  // key / cache index, it MUST sanitize the kid first. Path-traversal
236
236
  // (`../etc/passwd`), null-byte (`key\0..`), control chars, and
237
- // similar shapes turn a kid lookup into an arbitrary-file-read
238
- // primitive (CVE-2018-0114 java-jwt class). Use
237
+ // similar shapes turn a kid lookup into an arbitrary-file-read or
238
+ // SQLi primitive (the PortSwigger JWT "kid" injection / LFI class). Use
239
239
  // `b.guardJwt.kidSafe(header.kid)` — throws on traversal indicators
240
240
  // and control bytes, returns the validated kid on success.
241
241
  var key;
package/lib/auth/oauth.js CHANGED
@@ -1008,7 +1008,7 @@ function create(opts) {
1008
1008
  throw new OAuthError("auth-oauth/no-id-token", "verifyIdToken: idToken must be a string");
1009
1009
  }
1010
1010
  var parts = idToken.split(".");
1011
- // CVE-2026-29000 / CVE-2026-22817 / CVE-2026-23993 — mirror
1011
+ // CVE-2026-29000 / CVE-2026-22817 — mirror
1012
1012
  // jwt-external's 5-segment JWE refusal. A 5-segment compact
1013
1013
  // serialization is a JWE (RFC 7516); verifyIdToken is a JWS verifier
1014
1014
  // and a JWE shape reaching here is the confused-deputy class an OP
@@ -1023,7 +1023,7 @@ function create(opts) {
1023
1023
  }); } catch (_e) { /* drop-silent — observability sink */ }
1024
1024
  throw new OAuthError("auth-oauth/jwe-refused",
1025
1025
  "5-segment JWE id_token refused — verifyIdToken only handles JWS " +
1026
- "(CVE-2026-29000 / CVE-2026-23993 / CVE-2026-22817 / CVE-2026-34950 JWE-bypass class)");
1026
+ "(CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 JWE-bypass class)");
1027
1027
  }
1028
1028
  if (parts.length !== 3) {
1029
1029
  throw new OAuthError("auth-oauth/malformed-jwt", "ID token does not have 3 parts");
@@ -1039,13 +1039,14 @@ function create(opts) {
1039
1039
  if (!header || typeof header.alg !== "string") {
1040
1040
  throw new OAuthError("auth-oauth/malformed-jwt", "ID token header missing 'alg'");
1041
1041
  }
1042
- // CVE-2026-23993 — refuse unknown alg BEFORE any key resolution.
1042
+ // Alg-allowlist gate (CWE-347 / CWE-757) — refuse unknown alg BEFORE
1043
+ // any key resolution.
1043
1044
  // The acceptedAlgorithms list is the operator's posture; an alg
1044
1045
  // outside it never reaches the JWKS lookup or node:crypto.verify.
1045
1046
  if (acceptedAlgorithms.indexOf(header.alg) === -1) {
1046
1047
  throw new OAuthError("auth-oauth/alg-not-accepted",
1047
1048
  "ID token signed with '" + header.alg + "' which is not in the accepted-algorithm list " +
1048
- "(CVE-2026-23993 — refused before key lookup)");
1049
+ "(alg-allowlist gate — refused before key lookup)");
1049
1050
  }
1050
1051
  // RFC 7515 §4.1.11 — refuse JWS with `crit` header. Every other
1051
1052
  // verifier in the framework (jwt.js, jwt-external.js, dpop.js)
@@ -1344,8 +1345,8 @@ function create(opts) {
1344
1345
  }
1345
1346
  var iss = u.searchParams.get("iss");
1346
1347
  var sid = u.searchParams.get("sid");
1347
- // RFC 0 invariant: `iss` MUST match the configured issuer when
1348
- // present (defends against an attacker-controlled IdP forging a
1348
+ // OpenID Connect Front-Channel Logout 1.0 §3: `iss` MUST match the
1349
+ // configured issuer when present (defends against an attacker-controlled IdP forging a
1349
1350
  // logout for a session at a different IdP). `sid` is required
1350
1351
  // when the RP registered with frontchannel_logout_session_required=true;
1351
1352
  // we surface it either way and let the operator decide.
@@ -108,14 +108,14 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
108
108
  throw new AuthError("auth-oid4vci/wrong-proof-typ",
109
109
  "credential issuance: proof JWT typ must be \"openid4vci-proof+jwt\" (got \"" + header.typ + "\")");
110
110
  }
111
- // CVE-2026-23993 — refuse unknown / unsupported alg BEFORE any
112
- // verify-side work. The supportedAlgs list is the issuer's posture;
111
+ // Alg-allowlist gate (CWE-347 / CWE-757) — refuse unknown / unsupported
112
+ // alg BEFORE any verify-side work. The supportedAlgs list is the issuer's posture;
113
113
  // refusing here mirrors the discipline in oauth.verifyIdToken /
114
114
  // jwt-external.verifyExternal.
115
115
  if (!header.alg || supportedAlgs.indexOf(header.alg) === -1) {
116
116
  throw new AuthError("auth-oid4vci/unsupported-proof-alg",
117
117
  "credential issuance: proof JWT alg \"" + header.alg + "\" not in issuer-supported set " +
118
- "(CVE-2026-23993 — refused before key lookup)");
118
+ "(alg-allowlist gate — refused before key lookup)");
119
119
  }
120
120
  // AUTH-5 / RFC 7515 §4.1.11 — refuse non-empty `crit`. Pre-v0.9.x
121
121
  // silently ignored, letting an attacker-controlled wallet declare
package/lib/auth/saml.js CHANGED
@@ -453,12 +453,14 @@ function create(opts) {
453
453
 
454
454
  // XSW defense — refuse duplicate top-level security-critical
455
455
  // elements. SAML XML signature wrapping (XSW) attacks shuffle
456
- // signed elements alongside unsigned siblings; the parser's
457
- // first-match `_findChild` lookup combined with the signed-
458
- // element-ID check at L479 was vulnerable to a multi-Assertion
459
- // payload where the verifier signed one but the consumer read
460
- // attributes from another. Reject any Response with more than
461
- // one of these structural children (Audit 2026-05-11).
456
+ // signed elements alongside unsigned siblings; a first-match
457
+ // child lookup combined with a signed-element-ID check is
458
+ // vulnerable to a multi-Assertion payload where the verifier
459
+ // signs one element but the consumer reads attributes from
460
+ // another. This is the class behind CVE-2024-45409 (ruby-saml,
461
+ // CVSS 10.0, actively exploited) and CVE-2025-25291/25292
462
+ // (omniauth-saml / ruby-saml namespace-confusion XSW). Reject
463
+ // any Response with more than one of these structural children.
462
464
  var statusChildren = _findAllChildren(root, "Status", SAML_NS.protocol);
463
465
  if (statusChildren.length > 1) {
464
466
  throw new AuthError("auth-saml/duplicate-status",
@@ -1531,9 +1533,10 @@ function create(opts) {
1531
1533
  // http://www.w3.org/2009/xmlenc11#rsa-oaep (XMLEnc 1.1 §5.4.2)
1532
1534
  //
1533
1535
  // AES-CBC content encryption (xmlenc#aes128-cbc / aes256-cbc) is
1534
- // intentionally REFUSED: CVE-2011-1473 + the broader XML-Encryption
1535
- // padding-oracle research (Jager & Somorovsky 2011) demonstrate that
1536
- // CBC mode under XMLEnc is exploitable without per-content MAC.
1536
+ // intentionally REFUSED: the XML-Encryption padding-oracle research
1537
+ // (Jager & Somorovsky, "How to Break XML Encryption", CCS 2011)
1538
+ // demonstrates that CBC mode under XMLEnc is exploitable without per-
1539
+ // content MAC.
1537
1540
  // Operators integrating with IdPs that default to CBC (older ADFS /
1538
1541
  // Azure AD / Okta / Keycloak / OneLogin) MUST switch the IdP's
1539
1542
  // content-encryption setting to AES-128-GCM or AES-256-GCM. The
@@ -1543,7 +1546,7 @@ function create(opts) {
1543
1546
  //
1544
1547
  // SHA-1 anywhere (rsa-oaep-mgf1p with SHA-1 OAEP DigestMethod,
1545
1548
  // xmldsig#sha1 DigestMethod) is also refused — Bleichenbacher /
1546
- // collision risk plus CVE-2023-49141-class advisories outweigh
1549
+ // collision risk plus CVE-2023-49141 class advisories outweigh
1547
1550
  // "interop with stale IdPs". Operators upgrade the IdP's digest
1548
1551
  // algorithm to SHA-256+ rather than relax the framework defense.
1549
1552
  //
@@ -1606,7 +1609,7 @@ function _decryptEncryptedAssertion(encAssertion, spPrivateKeyPem) {
1606
1609
  }
1607
1610
  if (oaepHashName === "sha1") {
1608
1611
  throw new AuthError("auth-saml/encrypted-weak-oaep-digest",
1609
- "EncryptedKey OAEP DigestMethod is SHA-1 — refused (CVE-2023-49141-class). " +
1612
+ "EncryptedKey OAEP DigestMethod is SHA-1 — refused (CVE-2023-49141 class). " +
1610
1613
  "Require SHA-256+ on IdP side.");
1611
1614
  }
1612
1615
  var spKey;
@@ -1710,7 +1713,7 @@ function _decryptEncryptedAssertion(encAssertion, spPrivateKeyPem) {
1710
1713
  "(supported: W3C xmlenc11#aes128-gcm, xmlenc11#aes256-gcm, " +
1711
1714
  "framework-experimental urn:blamejs:experimental:xmlenc:xchacha20-poly1305). " +
1712
1715
  "AES-CBC content encryption is refused — switch the IdP to AES-128-GCM or AES-256-GCM " +
1713
- "(CVE-2011-1473 padding-oracle class).");
1716
+ "(XMLEnc CBC padding-oracle class, Jager & Somorovsky CCS 2011).");
1714
1717
  }
1715
1718
  return clearBytes.toString("utf8");
1716
1719
  }
@@ -441,15 +441,15 @@ async function verify(presentation, opts) {
441
441
  "verify: malformed JWT header: " + e.message);
442
442
  }
443
443
  var alg = headerObj.alg;
444
- // CVE-2026-23993 — refuse unknown / unsupported alg BEFORE any key
445
- // resolution. The shared `_assertAlgKtyMatch` helper repeats this
444
+ // Alg-allowlist gate (CWE-347 / CWE-757) — refuse unknown / unsupported
445
+ // alg BEFORE any key resolution. The shared `_assertAlgKtyMatch` helper repeats this
446
446
  // check after the issuer key is resolved; doing it here too closes
447
447
  // the gap where an issuerKeyResolver with side effects (network
448
448
  // fetch, audit emit) would run even when the alg is unsupported.
449
449
  if (typeof alg !== "string" || SUPPORTED_ALGS.indexOf(alg) === -1) {
450
450
  throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
451
451
  "verify: header alg \"" + alg + "\" not in supported set " +
452
- "(CVE-2026-23993 — refused before key lookup)");
452
+ "(alg-allowlist gate — refused before key lookup)");
453
453
  }
454
454
  // draft-ietf-oauth-sd-jwt-vc §3.1 — typ MUST be `vc+sd-jwt` (or
455
455
  // `dc+sd-jwt` for digital-credential profile). Pre-v0.9.x the absent-
package/lib/calendar.js CHANGED
@@ -354,7 +354,14 @@ function validate(jsCal) {
354
354
  * // → { "@type":"Event", uid:"a@b", updated:"2026-05-21T10:00:00Z", ... }
355
355
  */
356
356
  function fromIcal(text, opts) {
357
- var ast = safeIcal.parse(text, opts || {});
357
+ // Forward parser options to b.safeIcal.parse. Accept BOTH the
358
+ // documented nested form (`{ safeIcalOpts: { profile, caps, ... } }`)
359
+ // AND the historically-working top-level form (`{ profile: "balanced" }`)
360
+ // so neither caller regresses; the nested form wins on conflict. The
361
+ // `safeIcalOpts` wrapper key itself is stripped before forwarding.
362
+ var icalOpts = Object.assign({}, opts || {}, (opts && opts.safeIcalOpts) || {});
363
+ delete icalOpts.safeIcalOpts;
364
+ var ast = safeIcal.parse(text, icalOpts);
358
365
  var events = (ast && ast.vcalendar && ast.vcalendar.vevent) || [];
359
366
  var todos = (ast && ast.vcalendar && ast.vcalendar.vtodo) || [];
360
367
  var journals = (ast && ast.vcalendar && ast.vcalendar.vjournal) || [];
@@ -515,7 +522,7 @@ function toIcal(jsCal, opts) {
515
522
  * timestamps in the operator's `[from, to]` window. Returns an array
516
523
  * of ISO 8601 UTC strings (`yyyy-mm-ddTHH:MM:SSZ`). Bounded by
517
524
  * `MAX_EXPAND_INSTANCES` (4096) + `MAX_EXPAND_SPAN_MS` (10 years) to
518
- * defend against CVE-2024-39687-class recurrence-bomb expansion.
525
+ * defend against the RRULE recurrence-bomb expansion class.
519
526
  *
520
527
  * v1 supports FREQ=DAILY/WEEKLY/MONTHLY/YEARLY with INTERVAL, COUNT,
521
528
  * UNTIL. BYDAY / BYMONTH / BYMONTHDAY / BYWEEKNO / BYYEARDAY /
@@ -42,10 +42,11 @@ var retryHelper = require("./retry");
42
42
  * with the framework's `create(opts)` vocabulary.
43
43
  *
44
44
  * The `CIRCUIT_OPEN` error code is a pre-v1 artifact — every other
45
- * framework error class uses namespaced codes (`retry/...`). The
46
- * rename is deferred to v0.10 with a deprecation cycle so existing
47
- * operators who match `err.code === "CIRCUIT_OPEN"` aren't broken
48
- * in a patch.
45
+ * framework error class uses namespaced codes (`retry/...`). It is
46
+ * kept through the pre-1.0 line so existing operators who match
47
+ * `err.code === "CIRCUIT_OPEN"` aren't broken in a patch; the rename
48
+ * to a namespaced code lands at v1.0 alongside the namespaced-error
49
+ * sweep, with a deprecation warning shipping a minor ahead.
49
50
  *
50
51
  * @opts
51
52
  * name: string, // identifier used in audit + state-change events
@@ -5,7 +5,7 @@
5
5
  * Suite (PQC-first per framework crypto policy):
6
6
  * KEM: ML-KEM-1024 (FIPS 203) — post-quantum encapsulation
7
7
  * KDF: HKDF-SHA3-512
8
- * AEAD: ChaCha20-Poly1305 (RFC 7539)
8
+ * AEAD: ChaCha20-Poly1305 (RFC 8439, obsoletes RFC 7539)
9
9
  *
10
10
  * The classical HPKE suites in RFC 9180 §7 (DHKEM with X25519 / P-256 /
11
11
  * P-384 / P-521 + HKDF-SHA256/384/512 + AES-GCM/ChaCha20) are NOT
@@ -30,14 +30,14 @@
30
30
  * <code>suite(name)</code> returns the suite for one of the RFC 9497
31
31
  * ciphersuites — <code>ristretto255-sha512</code> (the Privacy Pass
32
32
  * default), <code>p256-sha256</code>, <code>p384-sha384</code>, or
33
- * <code>p521-sha512</code> — each exposing the three modes. Group and
33
+ * <code>p521-sha512</code> — each exposing both shipped modes. Group and
34
34
  * hash-to-curve operations come from the vendored <code>@noble/curves</code>.
35
35
  * Byte arguments are <code>Uint8Array</code> / <code>Buffer</code>;
36
36
  * returned elements and outputs are <code>Uint8Array</code>.
37
37
  *
38
38
  * @card
39
39
  * RFC 9497 Oblivious PRFs — learn <code>F(key, input)</code> without the
40
- * server seeing the input (oprf / voprf / poprf modes; ristretto255 / P-256
40
+ * server seeing the input (oprf / voprf modes; ristretto255 / P-256
41
41
  * / P-384 / P-521 suites). The primitive behind Privacy Pass, password
42
42
  * hardening, and private set intersection.
43
43
  */
package/lib/crypto.js CHANGED
@@ -789,7 +789,7 @@ function toBase64Url(buf) {
789
789
  *
790
790
  * Strict mode (default) refuses non-canonical input — chars outside
791
791
  * the RFC 4648 §5 alphabet, length-mod-4-of-1, mixed `+/` from
792
- * standard base64, trailing garbage. Defends a CVE-2022-0235-class
792
+ * standard base64, trailing garbage. Defends a CVE-2022-0235 class
793
793
  * footgun where Node's permissive decoder silently tolerated
794
794
  * tampered JWT signatures. Operators with a documented lossy legacy
795
795
  * payload opt out per call via `{ strict: false }`.
@@ -817,7 +817,7 @@ function fromBase64Url(s, opts) {
817
817
  // OAuth `state` round-tripping) MUST reject non-canonical / malformed
818
818
  // input. The Node base64url decoder silently tolerates trailing
819
819
  // garbage, mixed `+/` from standard base64, missing padding errors,
820
- // and length-mod-4 shapes — CVE-2022-0235-class footgun. Strict mode
820
+ // and length-mod-4 shapes — CVE-2022-0235 class footgun. Strict mode
821
821
  // (the default) refuses anything outside the RFC 4648 §5 alphabet +
822
822
  // length rules. Operators with a known-lossy legacy payload pass
823
823
  // `{ strict: false }` to opt out per call.
@@ -290,7 +290,8 @@ var GuardTimeError = defineClass("GuardTimeError", { alwaysPermane
290
290
  var GuardMimeError = defineClass("GuardMimeError", { alwaysPermanent: true });
291
291
  // GuardJwtError covers JWT identifier violations: shape malformation
292
292
  // (not 3 base64url segments), alg=none refuse (canonical CVE-class —
293
- // CVE-2015-9235 jsonwebtoken / CVE-2018-0114 java-jwt), alg-allowlist
293
+ // CVE-2015-9235 jsonwebtoken alg:none / CVE-2018-0114 Cisco node-jose
294
+ // embedded-JWK key confusion), alg-allowlist
294
295
  // drift, kid path-traversal (operator keyResolver path-injection
295
296
  // class), typ confusion, oversized header / payload / signature,
296
297
  // exp / nbf / iat sanity, missing required claims, unknown crit
package/lib/guard-jwt.js CHANGED
@@ -16,8 +16,9 @@
16
16
  *
17
17
  * Algorithm-confusion defense: `alg=none` is universally refused
18
18
  * at every profile (RFC 7518 §3.6 explicit-no-signature, the
19
- * canonical CVE-2015-9235 jsonwebtoken / CVE-2018-0114 java-jwt
20
- * class). The operator-supplied `allowedAlgs` allowlist defaults
19
+ * canonical CVE-2015-9235 jsonwebtoken alg:none / CVE-2018-0114
20
+ * Cisco node-jose embedded-JWK confusion class). The
21
+ * operator-supplied `allowedAlgs` allowlist defaults
21
22
  * to the framework's PQC-first set (ML-DSA-87 / ML-DSA-65 /
22
23
  * ML-DSA-44 / SLH-DSA-SHAKE-256{f,s} / SLH-DSA-SHA2-256{f,s} /
23
24
  * EdDSA / ES* / RS* / PS*) so HS256-against-RSA-public-key
@@ -493,7 +493,7 @@ function gate(opts) {
493
493
  *
494
494
  * Scan a DATA-body byte buffer for the SMTP smuggling shape per
495
495
  * CVE-2023-51764 (Postfix), CVE-2023-51765 (Sendmail), CVE-2023-51766
496
- * (Exim), CVE-2024-32178 (.NET System.Net.Mail). RFC 5321 §2.3.8
496
+ * (Exim). RFC 5321 §2.3.8
497
497
  * mandates canonical CRLF line termination; the smuggling exploit
498
498
  * relies on parsers that accept `\n.\n` (bare LF before / after the
499
499
  * dot) as an alternate body terminator and then resume parsing the
@@ -517,7 +517,7 @@ function detectBodySmuggling(buf) {
517
517
  throw new GuardSmtpCommandError("guard-smtp-command/bad-input",
518
518
  "detectBodySmuggling: input must be a Buffer");
519
519
  }
520
- // The CVE-2023-51764 / 51765 / 51766 / 2024-32178 class is any
520
+ // The CVE-2023-51764 / 51765 / 51766 class is any
521
521
  // dot-line whose line boundary is anything OTHER than canonical
522
522
  // \r\n on BOTH sides of the dot. The canonical-and-only terminator
523
523
  // is `\r\n.\r\n`. Every other shape that some receiver might honor
package/lib/mail-auth.js CHANGED
@@ -1858,8 +1858,8 @@ function dmarcParseAggregateReport(input, opts) {
1858
1858
  // "stream is malformed" (operator-level diagnostic) so audit/
1859
1859
  // alert wiring can react differently. Node surfaces the bomb
1860
1860
  // case with ERR_BUFFER_TOO_LARGE / "Output length exceeded the
1861
- // limit" / the explicit `maxOutputLength` code. CVE-class:
1862
- // CVE-2024-zlib decompression amplification.
1861
+ // limit" / the explicit `maxOutputLength` code. Defends the
1862
+ // decompression-amplification class (CWE-409 / CVE-2025-0725).
1863
1863
  var msg = (e && e.message) || String(e);
1864
1864
  var isBomb = (e && (e.code === "ERR_BUFFER_TOO_LARGE" ||
1865
1865
  e.code === "ERR_OUT_OF_RANGE")) ||
@@ -663,7 +663,7 @@ function _parseSignaturePacket(packetBytes) {
663
663
  if (hashAlg !== HASH_ALG_SHA256 && hashAlg !== HASH_ALG_SHA512) {
664
664
  throw new MailCryptoError("mail-crypto/pgp/bad-hash",
665
665
  "hash alg " + hashAlg + " refused; only SHA-256 (8) and SHA-512 (10) are accepted. " +
666
- "SHA-1 (id=2) refused per SHAttered (CVE-2017-9006-class).");
666
+ "SHA-1 (id=2) refused per SHAttered (2017 SHA-1 collision).");
667
667
  }
668
668
  var hashedSubLen = body.readUInt16BE(4);
669
669
  if (6 + hashedSubLen > body.length) {
@@ -21,9 +21,9 @@
21
21
  * second part as base64-encoded DER.
22
22
  *
23
23
  * Posture (when the surface lights up):
24
- * - Refuses SHA-1 as the signature hash (CVE-2017-9006-class
25
- * PKCS#7 collision attacks against legacy S/MIME) and as the
26
- * certificate signature algorithm.
24
+ * - Refuses SHA-1 as the signature hash (SHAttered, 2017 — practical
25
+ * SHA-1 collision; RFC 8551 §2.5 mandates SHA-256+ for S/MIME) and
26
+ * as the certificate signature algorithm.
27
27
  * - Refuses RSA keys < 2048 bits (RFC 8301 §3.1 — same posture
28
28
  * as the rest of the mail surface).
29
29
  * - Refuses MD5 anywhere (the historical S/MIME-v2 default; long
@@ -87,8 +87,8 @@
87
87
  * CVE citations:
88
88
  * - CVE-2017-17688 / CVE-2017-17689 (EFAIL — S/MIME variant; informs
89
89
  * the encrypt+decrypt deferral when that surface lights up)
90
- * - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass
91
- * classinforms the SHA-1 refusal posture)
90
+ * - SHAttered (2017 practical SHA-1 collision) + RFC 8551 §2.5 (SHA-256
91
+ * floor for S/MIME) inform the SHA-1 signature-hash refusal posture
92
92
  * - CVE-2018-5407 (PortSmash — informs the side-channel hardening
93
93
  * posture when private operations land in v2)
94
94
  */
@@ -109,7 +109,7 @@ var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
109
109
  // hand-copying strings. These reflect RFC 8551 §2.5 + RFC 8301 floors.
110
110
  var RSA_MIN_BITS = 2048; // allow:raw-byte-literal — RFC 8301 §3.1
111
111
  var ALLOWED_HASHES = ["sha256", "sha384", "sha512"];
112
- var REFUSED_HASHES = ["md5", "sha1"]; // allow:raw-byte-literal — CVE-2017-9006-class
112
+ var REFUSED_HASHES = ["md5", "sha1"]; // allow:raw-byte-literal — SHAttered / RFC 8551 §2.5
113
113
 
114
114
  // PROFILES + COMPLIANCE_POSTURES — the framework's standard cross-
115
115
  // primitive contract. sign() and verify() (live since v0.10.16) read
@@ -708,7 +708,7 @@ function checkCert(opts) {
708
708
  throw new MailCryptoError("mail-crypto/smime/refused-hash",
709
709
  "cert signature algorithm '" + sigAlgName +
710
710
  "' refused — SHA-1 / MD5 in cert signatures is forbidden " +
711
- "(CVE-2017-9006-class). Acceptable hashes: " + ALLOWED_HASHES.join(", "));
711
+ "(SHAttered SHA-1 collision; RFC 8551 §2.5). Acceptable hashes: " + ALLOWED_HASHES.join(", "));
712
712
  }
713
713
  }
714
714
 
@@ -62,7 +62,7 @@
62
62
  *
63
63
  * CVE citations:
64
64
  * - CVE-2017-17688 / CVE-2017-17689 (EFAIL)
65
- * - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass class)
65
+ * - SHAttered (2017 SHA-1 collision) + RFC 8551 §2.5 — SHA-1 signature-hash refusal
66
66
  */
67
67
 
68
68
  var pgp = require("./mail-crypto-pgp");
package/lib/mail-dav.js CHANGED
@@ -112,8 +112,9 @@
112
112
  * ## CVE defense composition
113
113
  *
114
114
  * - `b.safeIcal` rejects RRULE COUNT > 10000 / BYxxx list > 24 →
115
- * defends CVE-2024-39687 (ical4j RRULE recursion / Outlook
116
- * calendar bomb) on the PUT path.
115
+ * defends the ical4j RRULE-recursion / recurrence-expansion DoS
116
+ * class (unbounded RRULE expansion exhausts CPU/memory) on the
117
+ * PUT path.
117
118
  * - `b.xmlC14n.parse` rejects DOCTYPE / ENTITY in the
118
119
  * PROPFIND / REPORT body → defends XXE / billion-laughs on the
119
120
  * query path.
@@ -125,7 +126,7 @@
125
126
  * mount under their HTTP router. Composes b.safeIcal / b.safeVcard
126
127
  * for PUT-body validation, b.xmlC14n.parse for PROPFIND / REPORT
127
128
  * bodies. Per-principal URL isolation; operator-supplied storage
128
- * backend. Defends CVE-2024-39687 at the PUT boundary.
129
+ * backend. Defends the RRULE-recursion expansion-DoS class at the PUT boundary.
129
130
  */
130
131
 
131
132
  var lazyRequire = require("./lazy-require");
@@ -749,7 +750,7 @@ function create(opts) {
749
750
  return _refuseStatus(res, 400, "PUT requires a component path");
750
751
  }
751
752
  var bodyBuf = await _readBodyBytes(req);
752
- // Validate iCal body via safeIcal — defends CVE-2024-39687 at the
753
+ // Validate iCal body via safeIcal — defends the RRULE-recursion expansion-DoS class at the
753
754
  // ingest boundary.
754
755
  try {
755
756
  safeIcal.parse(bodyBuf, {
@@ -527,8 +527,9 @@ function autoDiscoverXml(opts) {
527
527
  // brotli or the in-progress UTA-draft requires it.
528
528
 
529
529
  // Hard caps — defensive against CVE-2025-0725 (libcurl/zlib
530
- // integer overflow), CVE-2024-zlib decompression amplification, and
531
- // the §5.2 community ceiling (receivers commonly cap at 10 MiB).
530
+ // integer overflow) and the decompression-amplification class
531
+ // (CWE-409), plus the §5.2 community ceiling (receivers commonly cap
532
+ // at 10 MiB).
532
533
  var TLSRPT_MAX_COMPRESSED_BYTES = C.BYTES.mib(4); // allow:raw-byte-literal — 4 MiB compressed cap per §5.2 community practice
533
534
  var TLSRPT_MAX_DECOMPRESSED_BYTES = C.BYTES.mib(32); // allow:raw-byte-literal — 32 MiB decompressed cap (operators override via opts)
534
535
  var TLSRPT_MAX_RATIO = 50; // allow:raw-byte-literal — 50:1 compression ratio refusal
@@ -53,7 +53,7 @@
53
53
  * pipelined command queued before TLS is refused with
54
54
  * `BAD Pipelined post-STARTTLS not permitted`.
55
55
  *
56
- * - **Literal-injection (CVE-2018-19518 INC IMAP class)** —
56
+ * - **Literal-injection / command-continuation smuggling** —
57
57
  * `{n}` literal continuation MUST come on a line of its own
58
58
  * (per `b.guardImapCommand.detectLiteralSmuggling`); oversize
59
59
  * literals refused (default 64 MiB); LITERAL+ (RFC 7888) non-
@@ -106,7 +106,7 @@
106
106
  *
107
107
  * ## What v1 does NOT ship
108
108
  *
109
- * - **Calendars / Contacts (RFC 9610)**, **Sieve (RFC 9404)**,
109
+ * - **Calendars / Contacts (RFC 9610)**, **Sieve (RFC 9661)**,
110
110
  * **MDN (RFC 9007)** — opt-in capabilities.
111
111
  *
112
112
  * @card
@@ -95,7 +95,7 @@
95
95
  *
96
96
  * - **CHECKSCRIPT** (RFC 5804 §2.12) — parse-only verb. Operators
97
97
  * who want it compose `b.safeSieve.validate` directly via JMAP
98
- * `SieveScript/validate` (RFC 9404). The MTA-side ManageSieve
98
+ * `SieveScript/validate` (RFC 9661). The MTA-side ManageSieve
99
99
  * surface is `PUTSCRIPT` + `HAVESPACE`; CHECKSCRIPT adds a third
100
100
  * entry point with no operator demand yet.
101
101
  * - **UNAUTHENTICATE** (RFC 5804 §2.14) — exotic. Operators close
@@ -25,7 +25,7 @@
25
25
  *
26
26
  * ## Defenses baked in
27
27
  *
28
- * - **SMTP smuggling** (CVE-2023-51764 / CVE-2024-32178) — every
28
+ * - **SMTP smuggling** (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim) — every
29
29
  * wire line passes through `b.guardSmtpCommand.validate` which
30
30
  * refuses bare LF, bare CR, NUL, C0 controls, DEL, and oversize.
31
31
  * The DATA body's `\r\n.\r\n` terminator is matched on canonical
@@ -108,7 +108,7 @@
108
108
  * @card
109
109
  * Inbound SMTP / MX listener. RFC 5321 state machine with SMTP-
110
110
  * smuggling defense baked into the wire-protocol layer (RFC 5321
111
- * §2.3.8 + CVE-2023-51764 / CVE-2024-32178), open-relay refusal by
111
+ * §2.3.8 + CVE-2023-51764 / 51765 / 51766), open-relay refusal by
112
112
  * default, STARTTLS-stripping defense (CVE-2021-38371), and the
113
113
  * framework's mail-gate cascade (HELO / RBL / greylist /
114
114
  * guardEnvelope / DMARC / safeMime / guardEmail) running at the
@@ -260,8 +260,8 @@ function create(opts) {
260
260
 
261
261
  // Default-on operator-supplied-domain hardening. opts.localDomains
262
262
  // and the HELO / MAIL FROM / RCPT TO domain validations all route
263
- // through `b.guardDomain` for IDN homograph defense (CVE-2017-5469
264
- // class), special-use-domain refusal (RFC 6761), label-length cap
263
+ // through `b.guardDomain` for IDN homograph / Punycode-spoof defense
264
+ // (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
265
265
  // (RFC 1035 §2.3.4), and bare-IP-as-domain refusal (CVE-2021-22931
266
266
  // class). Operators with a closed-network deployment can pass
267
267
  // `guardDomain: false` to skip; the default keeps the protection on.
@@ -40,7 +40,7 @@
40
40
  *
41
41
  * ## Wire-protocol defenses (inherited from MX listener pattern)
42
42
  *
43
- * - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 / 2024-32178 /
43
+ * - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 /
44
44
  * RFC 5321 §2.3.8): every wire line through
45
45
  * `b.guardSmtpCommand.validate`; DATA-body terminator scan
46
46
  * through `b.safeSmtp.findDotTerminator` (strict-CRLF);
@@ -351,8 +351,8 @@ function create(opts) {
351
351
  }
352
352
 
353
353
  // Default-on guardDomain hardening for HELO / MAIL FROM / RCPT TO.
354
- // Same posture as mail-server-mx — IDN homograph (CVE-2017-5469
355
- // class), special-use-domain refusal (RFC 6761), label-length cap
354
+ // Same posture as mail-server-mx — IDN homograph / Punycode-spoof
355
+ // (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
356
356
  // (RFC 1035 §2.3.4), bare-IP-as-domain refusal (CVE-2021-22931
357
357
  // class). Operators with a closed-network deployment pass
358
358
  // `guardDomain: false` to skip; the default keeps protection on.
package/lib/mail-store.js CHANGED
@@ -50,7 +50,7 @@
50
50
  * of caller; only `b.legalHold.release` can flip the flag.
51
51
  *
52
52
  * Parses messages on append via `b.safeMime.parse` (bounded
53
- * substrate, defends CVE-2024-39929 + CVE-2025-30258). Validates
53
+ * substrate, defends CVE-2024-39929 + CVE-2026-26312). Validates
54
54
  * `Message-Id` via `b.guardMessageId.validate`.
55
55
  *
56
56
  * @card
@@ -526,7 +526,7 @@ function _appendMessage(args) {
526
526
  "appendMessage: folder '" + args.folderName + "' not found");
527
527
  }
528
528
 
529
- // Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-2025-30258.
529
+ // Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-2026-26312.
530
530
  var tree = safeMime.parse(buf, args.safeMimeOpts);
531
531
 
532
532
  // Extract canonical fields.
package/lib/mail.js CHANGED
@@ -1594,10 +1594,10 @@ function create(opts) {
1594
1594
  var auditOn = opts.audit !== false;
1595
1595
 
1596
1596
  // Default-on guardDomain hardening for every outbound recipient + the
1597
- // sender address. Refuses CVE-2017-5469-class IDN homograph spoofs in
1597
+ // sender address. Refuses IDN homograph / mixed-script-confusable spoofs in
1598
1598
  // recipient or from domains, RFC 6761 special-use domain names
1599
1599
  // (`.localhost`, `.test`, `.invalid`, `.example`) in production sends,
1600
- // RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931-class
1600
+ // RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931 class
1601
1601
  // bare-IP-as-domain (DNS-rebinding allowlist-bypass class). Operators
1602
1602
  // sending to address literals (`<x@[1.2.3.4]>`) — rare; mostly mailing-
1603
1603
  // list internals — pass `guardDomain: false` to opt out, or pass
@@ -462,12 +462,15 @@ function applyToContext(opts) {
462
462
  // setKeyShares(["X25519MLKEM768", "X25519"]) → string[] (after)
463
463
  // resetKeyShares() → restores default
464
464
 
465
- // RFC 9794 (PQ TLS Hybrid Key Exchange) named-group ordering. The
466
- // preferred groups (the first the peer mutually supports wins) put the
467
- // IANA-registered hybrid named groups ahead of the classical fallback:
465
+ // PQ/T-hybrid named-group ordering (RFC 9794 is the PQ/T-hybrid
466
+ // *terminology*; the TLS codepoints come from the IANA TLS Supported
467
+ // Groups registry + draft-kwiatkowski-tls-ecdhe-mlkem +
468
+ // draft-ietf-tls-hybrid-design). The preferred groups (the first the peer
469
+ // mutually supports wins) put the IANA-registered hybrid named groups
470
+ // ahead of the classical fallback:
468
471
  //
469
- // X25519MLKEM768 — codepoint 0x11EC, RFC 9794 default hybrid
470
- // SecP256r1MLKEM768 — codepoint 0x11EB, RFC 9794 optional hybrid
472
+ // X25519MLKEM768 — codepoint 0x11EC (IANA; draft-kwiatkowski-tls-ecdhe-mlkem)
473
+ // SecP256r1MLKEM768 — codepoint 0x11EB (IANA; NIST-curve hybrid)
471
474
  // (NIST-curve fallback for FIPS-mandated peers
472
475
  // that refuse X25519)
473
476
  // SecP384r1MLKEM1024 — draft-kwiatkowski-tls-ecdhe-mlkem-02 codepoint
@@ -520,7 +523,7 @@ function resetKeyShares() {
520
523
  return getKeyShares();
521
524
  }
522
525
 
523
- // preferredGroups — RFC 9794 alias surface for the named-group list.
526
+ // preferredGroups — alias surface for the named-group list.
524
527
  // `set(list)` overrides the default ordering; `get()` reads the active
525
528
  // list; `reset()` restores the framework default. The setKeyShares /
526
529
  // getKeyShares / resetKeyShares names are kept as the lower-level
@@ -604,7 +607,7 @@ function buildOptions(opts) {
604
607
 
605
608
  // PQC group preference. Caller may narrow (drop a group) but not
606
609
  // widen — every requested group must appear in the framework
607
- // preferred list. Both `groups` (RFC 9794 alias) and `ecdhCurve`
610
+ // preferred list. Both `groups` (alias) and `ecdhCurve`
608
611
  // (Node TLS option) are accepted; `groups` wins when both supplied.
609
612
  var requested = null;
610
613
  if (Array.isArray(opts.groups)) {
@@ -54,10 +54,12 @@
54
54
  * bytes ever cross the audit boundary on the bomb-class path.
55
55
  *
56
56
  * Threat model:
57
- * - **CVE-2025-0725** (libcurl + zlib decompression amplification)
58
- * bounded output + ratio cap defeat the amplification.
59
- * - **CVE-2024-zlib** class (decompression-bomb research, gzip /
60
- * deflate / brotli variants) bounded output prevents OOM.
57
+ * - **Decompression bomb** (CWE-409 improper handling of highly
58
+ * compressed data; the classic 42.zip nested-bomb expands to
59
+ * petabytes from kilobytes) across gzip / deflate / brotli —
60
+ * the bounded-output cap + expansion-ratio cap refuse before the
61
+ * allocation, so no decompressed bytes are ever materialized past
62
+ * the cap.
61
63
  * - **Efail-class** (CVE-2017-17688 / 17689) — operators decrypting
62
64
  * MIME parts compose `b.safeDecompress` on the inner deflate
63
65
  * streams; the bounded-output posture defeats the unbounded-
@@ -73,8 +75,8 @@
73
75
  * - [RFC 1951](https://www.rfc-editor.org/rfc/rfc1951) deflate
74
76
  * - [RFC 1952](https://www.rfc-editor.org/rfc/rfc1952) gzip
75
77
  * - [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932) brotli
76
- * - [CVE-2025-0725](https://nvd.nist.gov/vuln/detail/CVE-2025-0725)
77
- * - [CVE-2024-zlib](https://nvd.nist.gov/) decompression-bomb class
78
+ * - [CWE-409](https://cwe.mitre.org/data/definitions/409.html) improper
79
+ * handling of highly compressed data (decompression bomb)
78
80
  */
79
81
 
80
82
  var zlib = require("node:zlib");
package/lib/safe-ical.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * delivery-time iTIP processing, and the scheduling primitives that
16
16
  * compose against ical bytes.
17
17
  *
18
- * Defends `CVE-2024-39687` (ical4j RRULE recursion / "Outlook
18
+ * Defends the ical4j RRULE-recursion expansion-DoS class ("Outlook
19
19
  * calendar bomb" — a hostile RRULE with unbounded COUNT and
20
20
  * recursive BYxxx expansion can pin a CalDAV server's CPU at 100%
21
21
  * until the request times out). Caps:
@@ -36,8 +36,8 @@
36
36
  * instances than this cap.
37
37
  * - RRULE BYDAY / BYMONTH / BYMONTHDAY / BYHOUR / BYMINUTE /
38
38
  * BYSECOND / BYSETPOS / BYWEEKNO / BYYEARDAY list-length cap
39
- * (24 entries) — refused regardless of profile. CVE-2024-39687
40
- * achieves expansion blow-up by stacking long BYxxx lists.
39
+ * (24 entries) — refused regardless of profile. The recursion
40
+ * DoS achieves expansion blow-up by stacking long BYxxx lists.
41
41
  *
42
42
  * Header-injection / control-char defense: refuses NUL, C0 control
43
43
  * bytes (other than TAB inside QUOTED-PRINTABLE-shaped values), and
@@ -75,8 +75,8 @@
75
75
  * @card
76
76
  * Bounded RFC 5545 iCalendar parser — caps total bytes, nesting
77
77
  * depth, RRULE COUNT and BYxxx list-lengths; refuses NUL / C0 / DEL
78
- * inside property values; allowlists property names; defends
79
- * CVE-2024-39687 (ical4j RRULE recursion / Outlook calendar-bomb).
78
+ * inside property values; allowlists property names; defends the
79
+ * ical4j RRULE-recursion expansion-DoS class (Outlook calendar-bomb).
80
80
  */
81
81
 
82
82
  var C = require("./constants");
@@ -84,8 +84,8 @@ var { defineClass } = require("./framework-error");
84
84
 
85
85
  var SafeIcalError = defineClass("SafeIcalError", { alwaysPermanent: true });
86
86
 
87
- // RRULE caps are enforced regardless of profile — CVE-2024-39687 has
88
- // no safe permissive posture.
87
+ // RRULE caps are enforced regardless of profile — the recursion-DoS
88
+ // class has no safe permissive posture.
89
89
  var RRULE_MAX_COUNT = 10000; // allow:raw-byte-literal — RFC 5545 §3.3.10 recurrence-count cap
90
90
  var RRULE_MAX_BY_ENTRIES = 24; // allow:raw-byte-literal — BYxxx list-length cap
91
91
 
@@ -223,7 +223,7 @@ function parse(text, opts) {
223
223
  if (byteLen > caps.maxBytes) {
224
224
  throw new SafeIcalError("safe-ical/oversize-bytes",
225
225
  "safeIcal.parse: input " + byteLen + " bytes exceeds maxBytes=" + caps.maxBytes +
226
- " (CVE-2024-39687-class defense)");
226
+ " (calendar-bomb defense)");
227
227
  }
228
228
 
229
229
  var lines = _unfold(s, caps);
@@ -466,7 +466,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
466
466
  if (depth > ctx.caps.maxNestingDepth) {
467
467
  throw new SafeIcalError("safe-ical/oversize-nesting",
468
468
  "safeIcal.parse: nesting depth exceeds maxNestingDepth=" +
469
- ctx.caps.maxNestingDepth + " (CVE-2024-39687-class defense)");
469
+ ctx.caps.maxNestingDepth + " (calendar-bomb defense)");
470
470
  }
471
471
  ctx.componentCount += 1;
472
472
  if (ctx.componentCount > ctx.caps.maxComponents) {
@@ -516,7 +516,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
516
516
  "safeIcal.parse: unknown property '" + pn +
517
517
  "' (extend via opts.extraProperties or use X- prefix)");
518
518
  }
519
- // RRULE caps — CVE-2024-39687 defense.
519
+ // RRULE caps — recursion-DoS / calendar-bomb defense.
520
520
  if (pn === "RRULE" || pn === "EXRULE") {
521
521
  _validateRrule(ln.value);
522
522
  }
@@ -558,7 +558,7 @@ function _validateRrule(value) {
558
558
  if (!isFinite(n) || n < 0 || n > RRULE_MAX_COUNT) {
559
559
  throw new SafeIcalError("safe-ical/oversize-rrule-count",
560
560
  "safeIcal.parse: RRULE COUNT=" + val + " exceeds cap=" +
561
- RRULE_MAX_COUNT + " (CVE-2024-39687 defense)");
561
+ RRULE_MAX_COUNT + " (calendar-bomb defense)");
562
562
  }
563
563
  } else if (key === "BYDAY" || key === "BYMONTH" || key === "BYMONTHDAY" ||
564
564
  key === "BYHOUR" || key === "BYMINUTE" || key === "BYSECOND" ||
@@ -567,7 +567,7 @@ function _validateRrule(value) {
567
567
  if (entries.length > RRULE_MAX_BY_ENTRIES) {
568
568
  throw new SafeIcalError("safe-ical/oversize-rrule-by",
569
569
  "safeIcal.parse: RRULE " + key + " list length " + entries.length +
570
- " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (CVE-2024-39687 defense)");
570
+ " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (calendar-bomb defense)");
571
571
  }
572
572
  }
573
573
  }
package/lib/safe-mime.js CHANGED
@@ -27,7 +27,7 @@
27
27
  * crypto.
28
28
  *
29
29
  * Defends `CVE-2024-39929` (Exim MIME multipart parser) and
30
- * `CVE-2025-30258` (gnumail truncated-MIME-tree class) by capping
30
+ * `CVE-2026-26312` (Stalwart nested `message/rfc822` MIME OOM) by capping
31
31
  * total parts, nesting depth, boundary length, header bytes,
32
32
  * header-line bytes, decoded body bytes, message bytes — plus
33
33
  * charset + transfer-encoding allowlists.
@@ -41,7 +41,7 @@
41
41
  * incoming message above a threshold.
42
42
  *
43
43
  * @card
44
- * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2025-30258.
44
+ * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2026-26312.
45
45
  */
46
46
 
47
47
  var C = require("./constants");
@@ -332,13 +332,13 @@ function _parsePart(buf, ctx, depth) {
332
332
  if (depth > ctx.maxNestingDepth) {
333
333
  throw new SafeMimeError("safe-mime/oversize-nesting",
334
334
  "safeMime.parse: nesting depth exceeded maxNestingDepth=" + ctx.maxNestingDepth +
335
- " (CVE-2024-39929-class defense)");
335
+ " (CVE-2024-39929 class defense)");
336
336
  }
337
337
  ctx.partCount += 1;
338
338
  if (ctx.partCount > ctx.maxParts) {
339
339
  throw new SafeMimeError("safe-mime/oversize-part-count",
340
340
  "safeMime.parse: total parts exceeded maxParts=" + ctx.maxParts +
341
- " (CVE-2024-39929-class defense)");
341
+ " (CVE-2024-39929 class defense)");
342
342
  }
343
343
 
344
344
  var sep = _findHeaderBodySep(buf);
@@ -668,7 +668,7 @@ function _decodeRfc2047Words(value) {
668
668
  raw = Buffer.from(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g,
669
669
  function (__, hex) { return String.fromCharCode(parseInt(hex, 16)); }), "binary"); // allow:raw-byte-literal — parseInt radix 16, not bytes
670
670
  }
671
- // RFC 2047 §5 / CVE-2020-7244 header-injection defense — after
671
+ // RFC 2047 §5 encoded-word header-injection defense — after
672
672
  // base64 / Q-encoded decode, check the DECODED bytes for header
673
673
  // separators (CR, LF, NUL). A sender that base64-encodes
674
674
  // `\r\nBcc: attacker@x.com` would otherwise reach the consumer's
@@ -680,7 +680,7 @@ function _decodeRfc2047Words(value) {
680
680
  if (b === 0x0d /* CR */ || b === 0x0a /* LF */ || b === 0x00 /* NUL */) {
681
681
  throw new SafeMimeError("safe-mime/rfc2047-header-injection",
682
682
  "RFC 2047 encoded-word decoded to bytes containing CR/LF/NUL " +
683
- "(byte index " + bi + "); refusing per RFC 2047 §5 / CVE-2020-7244 class");
683
+ "(byte index " + bi + "); refusing per RFC 2047 §5 (encoded-word header injection)");
684
684
  }
685
685
  }
686
686
  return _decodeBufferAs(raw, charset);
package/lib/safe-sieve.js CHANGED
@@ -619,7 +619,7 @@ function parse(script, opts) {
619
619
  * Parse-only validation — returns `{ ok, requiredCaps, issues }`
620
620
  * shape mirroring the rest of the guard family. Operator-facing
621
621
  * primitives that want a JMAP-style `SieveScript/validate` response
622
- * (RFC 9404) compose this and surface `issues` directly.
622
+ * (RFC 9661 — JMAP for Sieve Scripts) compose this and surface `issues` directly.
623
623
  *
624
624
  * @opts
625
625
  * profile: "strict" | "balanced" | "permissive",
package/lib/safe-smtp.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * - RFC 5321 §2.3.8 — line termination MUST be CRLF
24
24
  * - RFC 5321 §4.5.2 — dot-stuffing on the SMTP body
25
25
  * - RFC 5321 §4.1.1.4 — DATA command terminates with `<CRLF>.<CRLF>`
26
- * - CVE-2023-51764 / -51765 / -51766 / 2024-32178 — SMTP
26
+ * - CVE-2023-51764 / -51765 / -51766 — SMTP
27
27
  * smuggling (parsers that accept bare-LF dot-terminators).
28
28
  * The guard primitive `b.guardSmtpCommand.detectBodySmuggling`
29
29
  * owns smuggling detection; the safe-* terminator scanner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.8",
3
+ "version": "0.13.10",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:fd9a8f72-de3b-4a8d-8059-9ff47ccebb1b",
5
+ "serialNumber": "urn:uuid:62fe872e-bfa6-4982-9eb9-f37d3f139663",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-27T05:52:19.814Z",
8
+ "timestamp": "2026-05-27T08:47:25.364Z",
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.13.8",
22
+ "bom-ref": "@blamejs/core@0.13.10",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.8",
25
+ "version": "0.13.10",
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.13.8",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.10",
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.13.8",
57
+ "ref": "@blamejs/core@0.13.10",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]