@blamejs/core 0.13.13 → 0.13.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/acme.js +8 -5
- package/lib/archive-read.js +24 -20
- package/lib/break-glass.js +4 -5
- package/lib/crypto.js +8 -5
- package/lib/guard-dsn.js +3 -2
- package/lib/guard-list-id.js +3 -2
- package/lib/guard-regex.js +4 -4
- package/lib/guard-smtp-command.js +1 -2
- package/lib/network-dnssec.js +98 -5
- package/lib/parsers/safe-toml.js +8 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.15 (2026-05-27) — **Corrected more source citations and made deferred/reserved options honest in their docs.** A second accuracy pass over source threat-annotations and option docs. Three citation corrections: the base64url strict-decode guard cited CVE-2022-0235 (which is actually a node-fetch cookie-leak, unrelated) — it now names the weakness class it defends (CWE-347 / CWE-1286 signature canonicalization); the glob consecutive-wildcard ReDoS cap cited the wrong library (the CVE-2026-26996 ReDoS is minimatch, not picomatch — the adjacent picomatch one is CVE-2026-33671); and CVE-2026-32178 is reframed to the CWE-138 header-injection-spoofing class the public record actually documents (and dropped from the end-of-data SMTP-smuggling list, which is a different class). Several options/statuses are now honest about not-yet-implemented surface: b.archive.read.zip.fromTrustedStream is marked experimental (its methods throw and its options aren't honored yet — the example now shows the supported buffer-then-random-access path); b.acme revokeCert's useCertKey / certPrivateKey are marked reserved (the cert-key path throws; account-key signing is the supported default); and a stale message claiming passkey break-glass factors were a future feature is removed (passkeys are a live allowed factor). No runtime behaviour changes beyond message/doc text. **Changed:** *Deferred / reserved surface now documented honestly* — `b.archive.read.zip.fromTrustedStream` is marked `experimental` — its `inspect`/`entries`/`extract` throw and its `bombPolicy`/`audit` options aren't honored yet; the documented example now shows the supported path (buffer the stream, then use the random-access reader). `b.acme` `revokeCert`'s `useCertKey` / `certPrivateKey` options are marked reserved (the cert-key-signed-revocation path throws; account-key signing, the default, covers mainstream CAs). A `b.breakGlass` policy error and comment that called passkey factors a future feature are corrected — passkeys are a live allowed factor. **Fixed:** *Corrected misattributed CVE citations in source threat-annotations* — `b.crypto.fromBase64Url`'s strict-decode guard cited CVE-2022-0235 (a node-fetch header-leak, unrelated to base64/JWT decoding); it now cites the weakness class it actually defends — CWE-347 / CWE-1286 signature canonicalization. `b.guardRegex`'s consecutive-`*` cap attributed CVE-2026-26996 to picomatch; that ReDoS is in minimatch (the picomatch ReDoS it also defends is CVE-2026-33671) — the library name is corrected. CVE-2026-32178 is reframed to the CWE-138 header-injection spoofing class the public advisory documents, and removed from the end-of-data SMTP-smuggling trio (a distinct class). No behaviour change — the defenses are unchanged.
|
|
12
|
+
|
|
13
|
+
- v0.13.14 (2026-05-27) — **DNSSEC chain validation now bounds KeyTrap (CVE-2023-50387) amplification with hard caps.** b.network.dns.dnssec.verifyChain tried every DNSKEY whose 16-bit key tag matched an RRSIG, with no cap on how many candidates or total signature verifications a single response could drive. A hostile zone publishing many DNSKEYs sharing one key tag (plus matching RRSIGs) could force O(keys x signatures) full public-key verifications from one query — the KeyTrap denial-of-service (CVE-2023-50387). Validation is now bounded by non-configurable caps that match the BIND / Unbound mitigations: at most 4 same-tag candidate keys are tried per RRSIG, at most 64 DNSKEYs per zone link and 16 DS records per delegation are accepted, the chain is at most 128 links deep, and the whole response is held to a signature-validation budget that scales with chain depth (so a legitimate deep delegation is never false-rejected while bounded collisions stay bounded); exceeding any of these refuses the response rather than performing the work. Separately, a domain name that encodes to more than 255 octets is now refused at canonicalization (RFC 1035 §2.3.4), which also bounds the NSEC3 closest-encloser label enumeration, and the NSEC3 iteration ceiling is lowered from 500 to 150 to match the BIND 9.16.33+ / Unbound 1.17.1 fix for the sibling CVE-2023-50868. **Security:** *`verifyChain` caps colliding-key fan-out and total signature validations (KeyTrap / CVE-2023-50387)* — A zone advertising many same-key-tag DNSKEYs and RRSIGs can no longer drive unbounded public-key verifications. New refusals: `dnssec/too-many-colliding-keys` (>4 same-tag candidates per RRSIG), `dnssec/too-many-dnskeys` (>64 DNSKEYs per zone link), `dnssec/too-many-ds` (>16 DS records per delegation), `dnssec/too-many-links` (chain deeper than 128), and `dnssec/validation-budget-exceeded` (signature validations beyond the depth-scaled budget). The caps are intentionally non-configurable — they sit well above any legitimate zone, and the budget scales with chain depth so deep delegations validate normally. · *Domain-name octet cap + lower NSEC3 iteration ceiling* — A name that canonicalizes to more than 255 octets is refused (`dnssec/bad-name`, RFC 1035 §2.3.4), which bounds the per-label NSEC3 closest-encloser enumeration (CVE-2023-50868 class). The default NSEC3 iteration ceiling drops from 500 to 150, matching the BIND 9.16.33+ / Unbound 1.17.1 post-CVE defaults (RFC 9276 recommends 0).
|
|
14
|
+
|
|
11
15
|
- v0.13.13 (2026-05-27) — **Archive extraction-path verification now refuses Windows reserved names, NTFS data streams, and trailing-dot/space per segment.** b.guardFilename.verifyExtractionPath (the per-entry gate b.archive.read.zip.extract / b.safeArchive run on every extracted file) checked traversal, absolute paths, drive-letter and UNC prefixes, null bytes, PATH_MAX overflow, and realpath containment — but not the per-segment Windows write-target hazards the disk validate / sanitize paths already reject. An archive entry named CON, NUL.txt, subdir/LPT1, file.txt:hidden, or secret.txt. stayed inside the extraction root, so the containment and realpath checks passed it, yet on Windows it would resolve to a device, write a hidden NTFS stream, or (after Windows strips the trailing dot/space) overwrite a sibling file. These are now refused: any path segment that collides with a Windows reserved device name, uses NTFS alternate-data-stream syntax (name:stream), or carries a trailing dot or leading/trailing whitespace. The checks are platform-unconditional — a verifier running on Linux still refuses names that are only dangerous on the Windows host that ultimately extracts the archive — with a per-check opt-out (reservedNamePolicy / adsPolicy / leadingTrailingPolicy: "allow") for Linux-only targets. **Security:** *`verifyExtractionPath` refuses per-segment Windows extraction hazards (reserved names / NTFS ADS / trailing dot-space)* — Closes a within-root write-target-redirection gap: an extracted entry could stay inside the destination yet, on Windows, resolve to a device (`CON` / `NUL` / `COM1` / `LPT1`), write a hidden alternate data stream (`file.txt:payload`), or overwrite a sibling after Windows strips a trailing dot/space (`config.`). The verification gate now rejects all three per path segment. Refusal is platform-unconditional (the verifier may run on a different OS than the extractor); set `reservedNamePolicy` / `adsPolicy` / `leadingTrailingPolicy` to `"allow"` to opt a check out on a Linux-only target. Single-entry, name-only residuals — 8.3 short-name aliasing, case-insensitive cross-entry collisions, and archive symlink/hardlink entry-target validation — remain the extract orchestrator's responsibility (it owns the case-folded seen-set and the link-target gate).
|
|
12
16
|
|
|
13
17
|
- v0.13.12 (2026-05-27) — **Inbound MX listener now runs the connection-level gate cascade it documented — HELO identity, DNS blocklist, and greylisting.** b.mail.server.mx.create documented helo / rbl / greylist gate options, but the listener never invoked them — an operator who wired them got silent acceptance of mail those gates would have rejected. They are now wired into the live SMTP state machine: the HELO-identity gate evaluates at HELO/EHLO and refuses a spoofed or malformed identity with 550; the DNS-blocklist gate evaluates the connecting IP once per connection and refuses a listed source with 554; the greylisting gate defers a first-seen (ip, sender, recipient) tuple with a 450 tempfail so legitimate senders retry and pass. Each gate is skipped when the operator doesn't wire it. Because these gates do DNS and store lookups, the per-connection command pump was reworked to process commands asynchronously and strictly in arrival order, so pipelined commands (RFC 2920) cannot overtake a gate still resolving and the existing SMTP-smuggling and STARTTLS-stripping defenses are unchanged. The message-authentication gate (SPF/DKIM/DMARC alignment via b.guardEnvelope) needs the inbound SPF + DKIM verification results as inputs; that inbound-auth pipeline lands as a follow-up, and the documentation no longer implies that gate is active today. **Added:** *HELO-identity / RBL / greylist gates wired into `b.mail.server.mx`* — When wired, `opts.helo` (FCrDNS / HELO-shape / self-name checks) refuses a bad HELO identity at HELO/EHLO with 550; `opts.rbl` refuses a connecting IP found on a DNS blocklist with 554 (evaluated once per connection); `opts.greylist` defers a first-seen (ip, sender, recipient) tuple with 450 4.7.1. Their verdicts surface on the `rcpt_to` event (`rblListed`, `greylist`) and the `helo` event (`heloVerdict`), with dedicated `helo_gate_refused` / `rbl_refused` / `greylist_deferred` audit events. A gate the operator doesn't supply is skipped, never synthesized. **Changed:** *MX command pump processes commands asynchronously and in arrival order* — Gate evaluation involves DNS and store lookups, so the per-connection command pump now awaits each command before the next. Pipelined commands are serialized so a gate resolving cannot let a later command answer ahead of an earlier one; reply ordering, the bare-LF SMTP-smuggling refusal, and the STARTTLS-stripping defense are unchanged. No change to the listener's external behaviour when no gates are wired. **Deprecated:** *SPF/DKIM/DMARC-alignment gate documentation corrected to match what is active* — The `envelope` (SPF/DKIM/DMARC alignment) and `dmarc` gate options were documented as wireable but require inbound SPF + DKIM verification results the listener does not yet produce. They are removed from the documented option set until the inbound-authentication pipeline (composing `b.mail.spf` + `b.mail.dmarc` + DKIM verification) lands; run those checks on the delivered message via the agent handoff in the meantime.
|
package/lib/acme.js
CHANGED
|
@@ -1079,14 +1079,17 @@ function create(opts) {
|
|
|
1079
1079
|
* the DER-encoded cert (base64url-encoded automatically) plus an
|
|
1080
1080
|
* optional `reason` code per RFC 5280 §5.3.1 (0=unspecified,
|
|
1081
1081
|
* 1=keyCompromise, 3=affiliationChanged, 4=superseded, 5=cessationOfOperation).
|
|
1082
|
-
* Signs with the account key
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1082
|
+
* Signs with the account key — the only supported path today, and
|
|
1083
|
+
* sufficient for mainstream CAs. (The cert-key-signed variant —
|
|
1084
|
+
* `useCertKey` / `certPrivateKey` — is reserved and not yet
|
|
1085
|
+
* implemented; passing `useCertKey:true` throws.)
|
|
1085
1086
|
*
|
|
1086
1087
|
* @opts
|
|
1087
1088
|
* reason: number, // RFC 5280 §5.3.1 reason code; default 0 (unspecified)
|
|
1088
|
-
* useCertKey: boolean, //
|
|
1089
|
-
*
|
|
1089
|
+
* useCertKey: boolean, // RESERVED — cert-key-signed revocation is not yet
|
|
1090
|
+
* // implemented; account-key signing (the default)
|
|
1091
|
+
* // covers mainstream CAs. Passing true throws.
|
|
1092
|
+
* certPrivateKey: KeyObject, // RESERVED — consumed only by the cert-key path above
|
|
1090
1093
|
*
|
|
1091
1094
|
* @example
|
|
1092
1095
|
* await acme.revokeCert(certDerBuffer, { reason: 4 }); // 4 = superseded
|
package/lib/archive-read.js
CHANGED
|
@@ -772,7 +772,7 @@ function zip(adapter, opts) {
|
|
|
772
772
|
* @primitive b.archive.read.zip.fromTrustedStream
|
|
773
773
|
* @signature b.archive.read.zip.fromTrustedStream(adapter, opts?)
|
|
774
774
|
* @since 0.12.7
|
|
775
|
-
* @status
|
|
775
|
+
* @status experimental
|
|
776
776
|
* @related b.archive.read.zip, b.archive.adapters.trustedStream
|
|
777
777
|
*
|
|
778
778
|
* Forward-scan-only ZIP reader for trusted Readable sources. No
|
|
@@ -781,20 +781,23 @@ function zip(adapter, opts) {
|
|
|
781
781
|
* `b.archive.zip().toStream()` output back into a reader for round-trip
|
|
782
782
|
* verification).
|
|
783
783
|
*
|
|
784
|
-
*
|
|
785
|
-
* `
|
|
784
|
+
* NOT YET IMPLEMENTED: the streaming LFH walker is not built —
|
|
785
|
+
* `inspect()` / `entries()` / `extract()` throw
|
|
786
|
+
* `archive-read/trusted-stream-*-deferred`, and `bombPolicy` / `audit`
|
|
787
|
+
* are accepted but not yet honored. Re-opens when a streaming
|
|
788
|
+
* consumer needs it. Until then, collect the stream into a buffer and
|
|
789
|
+
* use the random-access reader, which is the supported path for both
|
|
790
|
+
* trusted round-trip verification and adversarial input.
|
|
786
791
|
*
|
|
787
792
|
* @opts
|
|
788
|
-
* bombPolicy: {
|
|
789
|
-
*
|
|
790
|
-
* audit: b.audit,
|
|
793
|
+
* bombPolicy: { ... }, // reserved — not yet honored
|
|
794
|
+
* audit: b.audit, // reserved — not yet honored
|
|
791
795
|
*
|
|
792
796
|
* @example
|
|
793
|
-
*
|
|
794
|
-
* var
|
|
795
|
-
*
|
|
796
|
-
* );
|
|
797
|
-
* for await (var e of reader.entries()) console.log(e.name, e.size);
|
|
797
|
+
* // Supported path: buffer the stream, then read random-access.
|
|
798
|
+
* var bytes = await someStreamToBuffer(producedZipStream);
|
|
799
|
+
* var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes));
|
|
800
|
+
* var entries = await reader.inspect();
|
|
798
801
|
*/
|
|
799
802
|
function fromTrustedStream(adapter, opts) {
|
|
800
803
|
if (!adapter || adapter.kind !== "trusted-sequential") {
|
|
@@ -805,25 +808,26 @@ function fromTrustedStream(adapter, opts) {
|
|
|
805
808
|
var bombPolicy = _normalizeBombPolicy(opts.bombPolicy);
|
|
806
809
|
void bombPolicy;
|
|
807
810
|
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
811
|
+
// The streaming LFH walker is not built — only the API surface +
|
|
812
|
+
// adapter validation exist. Extraction via streaming inflate +
|
|
813
|
+
// data-descriptor scanning re-opens when a streaming consumer needs
|
|
814
|
+
// it; until then the supported path is to buffer the stream and use
|
|
815
|
+
// the random-access reader (which handles both trusted round-trip
|
|
816
|
+
// verification and adversarial input).
|
|
813
817
|
async function inspect() {
|
|
814
818
|
throw new ArchiveReadError("archive-read/trusted-stream-inspect-deferred",
|
|
815
|
-
"fromTrustedStream.inspect() is
|
|
816
|
-
"
|
|
819
|
+
"fromTrustedStream.inspect() is not implemented — collect the stream into a buffer and " +
|
|
820
|
+
"use b.archive.read.zip(b.archive.adapters.buffer(bytes))");
|
|
817
821
|
}
|
|
818
822
|
|
|
819
823
|
async function* entries() {
|
|
820
824
|
throw new ArchiveReadError("archive-read/trusted-stream-entries-deferred",
|
|
821
|
-
"fromTrustedStream.entries() is
|
|
825
|
+
"fromTrustedStream.entries() is not implemented — collect into a buffer and use the random-access reader");
|
|
822
826
|
}
|
|
823
827
|
|
|
824
828
|
async function extract() {
|
|
825
829
|
throw new ArchiveReadError("archive-read/trusted-stream-extract-deferred",
|
|
826
|
-
"fromTrustedStream.extract() is
|
|
830
|
+
"fromTrustedStream.extract() is not implemented — collect into a buffer and use the random-access reader");
|
|
827
831
|
}
|
|
828
832
|
|
|
829
833
|
return {
|
package/lib/break-glass.js
CHANGED
|
@@ -130,9 +130,9 @@ function _ensureFactorLockout() {
|
|
|
130
130
|
// keys + encryption-context binding (cross-cell tampering / accidental
|
|
131
131
|
// row-swap fails closed). It does NOT defend against vault-key
|
|
132
132
|
// compromise alone — the DEK is still vault-recoverable. True
|
|
133
|
-
// second-factor cryptographic gating
|
|
134
|
-
//
|
|
135
|
-
//
|
|
133
|
+
// second-factor cryptographic gating uses passkey integration (the
|
|
134
|
+
// passkey private key lives on the YubiKey, not in the framework, so a
|
|
135
|
+
// vault leak alone can't unwrap).
|
|
136
136
|
|
|
137
137
|
// In-memory DEK cache. Keyed by table name. Cleared on _resetForTest.
|
|
138
138
|
var dekCache = new Map();
|
|
@@ -520,8 +520,7 @@ function _validatePolicySet(table, opts) {
|
|
|
520
520
|
if (ALLOWED_FACTORS.indexOf(opts.factors[j]) === -1) {
|
|
521
521
|
throw new BreakGlassError("breakglass/bad-policy",
|
|
522
522
|
"policy.set: factors[" + j + "] '" + opts.factors[j] +
|
|
523
|
-
"' not in
|
|
524
|
-
" (passkey lands in v0.5.2)");
|
|
523
|
+
"' not in allowed factors [" + ALLOWED_FACTORS.join(",") + "]");
|
|
525
524
|
}
|
|
526
525
|
}
|
|
527
526
|
// Model B (cryptographic mode) ships in v0.5.1. When enabled,
|
package/lib/crypto.js
CHANGED
|
@@ -789,10 +789,12 @@ 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
|
|
793
|
-
* footgun where
|
|
794
|
-
*
|
|
795
|
-
*
|
|
792
|
+
* standard base64, trailing garbage. Defends the CWE-347 /
|
|
793
|
+
* CWE-1286 signature-canonicalization footgun where a permissive
|
|
794
|
+
* base64url decoder silently tolerates a tampered JWS / JWT signature
|
|
795
|
+
* (non-canonical bytes decoding to the same buffer). Operators with a
|
|
796
|
+
* documented lossy legacy payload opt out per call via
|
|
797
|
+
* `{ strict: false }`.
|
|
796
798
|
*
|
|
797
799
|
* @opts
|
|
798
800
|
* strict: boolean // default: true — refuse non-canonical input
|
|
@@ -817,7 +819,8 @@ function fromBase64Url(s, opts) {
|
|
|
817
819
|
// OAuth `state` round-tripping) MUST reject non-canonical / malformed
|
|
818
820
|
// input. The Node base64url decoder silently tolerates trailing
|
|
819
821
|
// garbage, mixed `+/` from standard base64, missing padding errors,
|
|
820
|
-
// and length-mod-4 shapes —
|
|
822
|
+
// and length-mod-4 shapes — the CWE-347 / CWE-1286 signature-
|
|
823
|
+
// canonicalization footgun. Strict mode
|
|
821
824
|
// (the default) refuses anything outside the RFC 4648 §5 alphabet +
|
|
822
825
|
// length rules. Operators with a known-lossy legacy payload pass
|
|
823
826
|
// `{ strict: false }` to opt out per call.
|
package/lib/guard-dsn.js
CHANGED
|
@@ -84,8 +84,9 @@
|
|
|
84
84
|
* generating a DSN (the existing `b.mail.bounce` primitive does
|
|
85
85
|
* this); this guard parses INBOUND DSNs and gates the parse
|
|
86
86
|
* surface bounds, not the bounce-generation policy.
|
|
87
|
-
* - **DSN header-injection class** (CVE-2026-32178 .NET
|
|
88
|
-
* System.Net.Mail
|
|
87
|
+
* - **DSN header-injection class** (CVE-2026-32178 — .NET CWE-138
|
|
88
|
+
* special-element / header-injection spoofing, the System.Net.Mail
|
|
89
|
+
* vector per MSRC, at outbound; the inbound parse path here)
|
|
89
90
|
* — refuses CR/LF/NUL/C0 in header lines.
|
|
90
91
|
* - **CSAF / iSchedule prose tampering** — operator inspecting
|
|
91
92
|
* the prose part for the original recipient runs into the
|
package/lib/guard-list-id.js
CHANGED
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
* octets. Total header value capped at 998 bytes per RFC 5322
|
|
38
38
|
* §2.1.1 line cap.
|
|
39
39
|
* - **CRLF + control-char refusal** — header-injection defense
|
|
40
|
-
* (CVE-2026-32178 .NET
|
|
41
|
-
*
|
|
40
|
+
* (CVE-2026-32178 — .NET CWE-138 header-injection spoofing, the
|
|
41
|
+
* System.Net.Mail vector per MSRC, on the wire-protocol surface;
|
|
42
|
+
* this primitive's job is the SEMANTIC shape).
|
|
42
43
|
* - **Phrase-injection refusal** — Operator-supplied display
|
|
43
44
|
* phrase mustn't carry CRLF / `<` / `>` outside the angle
|
|
44
45
|
* brackets (a separate Bcc/Cc header smuggled into the phrase
|
package/lib/guard-regex.js
CHANGED
|
@@ -248,14 +248,14 @@ function _detectIssues(input, opts) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Consecutive-star wildcard cap (CVE-2026-26996). Operator-supplied
|
|
251
|
-
// glob fragments compile to picomatch / RegExp; a long run
|
|
252
|
-
// against a non-matching literal walks O(4^N). Three-or-more
|
|
251
|
+
// glob fragments compile to minimatch / picomatch / RegExp; a long run
|
|
252
|
+
// of `*` against a non-matching literal walks O(4^N). Three-or-more
|
|
253
253
|
// consecutive `*` is the canonical bad shape; `**` (recursive glob)
|
|
254
254
|
// stays permitted, gated by the profile's `maxConsecutiveStars`.
|
|
255
255
|
function _detectConsecutiveStar(input, opts, issues) {
|
|
256
256
|
if (opts.consecutiveStarPolicy === "allow") return;
|
|
257
|
-
// CVE-2026-26996 is a
|
|
258
|
-
// `***+literal` walks O(4^N) when
|
|
257
|
+
// CVE-2026-26996 is a minimatch glob-shape backtracking class —
|
|
258
|
+
// `***+literal` walks O(4^N) when minimatch translates the run to a
|
|
259
259
|
// backtracking-heavy regex. Native ECMAScript regex syntax cannot
|
|
260
260
|
// produce three consecutive `*` quantifiers (it's a SyntaxError),
|
|
261
261
|
// so applying this detector to `inputKind: "regex"` strings only
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* ## Smuggling defense — bare-CR / bare-LF refusal
|
|
16
16
|
*
|
|
17
17
|
* The SMTP smuggling class (`CVE-2023-51764` Postfix, `CVE-2023-51765`
|
|
18
|
-
* Sendmail, `CVE-2023-51766` Exim
|
|
19
|
-
* `System.Net.Mail`) exploits implementations that accept the
|
|
18
|
+
* Sendmail, `CVE-2023-51766` Exim) exploits implementations that accept the
|
|
20
19
|
* non-standard end-of-data sequence `<LF>.<LF>` or `<LF>.<CR><LF>`
|
|
21
20
|
* instead of the standard `<CR><LF>.<CR><LF>`. The introduced break-
|
|
22
21
|
* out lets a malicious peer inject a second message past SPF / DMARC
|
package/lib/network-dnssec.js
CHANGED
|
@@ -101,7 +101,16 @@ function _canonicalName(name) {
|
|
|
101
101
|
parts.push(Buffer.from([lab.length]), lab);
|
|
102
102
|
}
|
|
103
103
|
parts.push(Buffer.from([0]));
|
|
104
|
-
|
|
104
|
+
var wire = Buffer.concat(parts);
|
|
105
|
+
// RFC 1035 §2.3.4 — a domain name is at most 255 octets on the wire.
|
|
106
|
+
// Enforcing it here also bounds the per-label count (and thus the NSEC3
|
|
107
|
+
// closest-encloser candidate enumeration, CVE-2023-50868 class), since
|
|
108
|
+
// each label costs at least 2 octets.
|
|
109
|
+
if (wire.length > 255) { // allow:raw-byte-literal — RFC 1035 total-name octet cap
|
|
110
|
+
throw new DnssecError("dnssec/bad-name",
|
|
111
|
+
"dnssec: name '" + name + "' encodes to " + wire.length + " octets, exceeds RFC 1035 cap of 255");
|
|
112
|
+
}
|
|
113
|
+
return wire;
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
function _u16(n) { return Buffer.from([(n >> 8) & 0xff, n & 0xff]); } // allow:raw-byte-literal — 16-bit big-endian split
|
|
@@ -340,7 +349,28 @@ var BASE32HEX = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; // allow:raw-byte-l
|
|
|
340
349
|
var TYPE_DS = 43; // allow:raw-byte-literal — IANA RR type DS
|
|
341
350
|
var TYPE_CNAME = 5;
|
|
342
351
|
var NSEC3_HASH_SHA1 = 1; // RFC 5155 §5 — the only registered NSEC3 hash
|
|
343
|
-
var DEFAULT_MAX_NSEC3_ITERATIONS =
|
|
352
|
+
var DEFAULT_MAX_NSEC3_ITERATIONS = 150; // allow:raw-time-literal — DoS ceiling on iterated SHA-1; matches BIND 9.16.33+/Unbound 1.17.1 post-CVE-2023-50868 (RFC 9276 wants 0; deployed zones still use >0)
|
|
353
|
+
|
|
354
|
+
// KeyTrap (CVE-2023-50387) amplification caps. A hostile zone can publish
|
|
355
|
+
// many DNSKEYs sharing one 16-bit key tag and many RRSIGs, forcing a
|
|
356
|
+
// validator into O(keys x sigs) full signature verifications from a single
|
|
357
|
+
// query. Bound both factors: the colliding-candidate fan-out per RRSIG, and
|
|
358
|
+
// the total signature-validation work. Matches the BIND
|
|
359
|
+
// `max-key-tag-collisions` + Unbound validation-budget mitigations.
|
|
360
|
+
//
|
|
361
|
+
// The per-response budget SCALES with declared chain depth so a legitimate
|
|
362
|
+
// deep delegation isn't false-rejected: a valid N-link chain does ~2N-1
|
|
363
|
+
// signature verifies (1 root DNSKEY + parent-DS + child-DNSKEY per child),
|
|
364
|
+
// so the budget is links.length * MAX_VALIDATIONS_PER_LINK (= 2 RRSIGs/link
|
|
365
|
+
// x MAX_COLLIDING_KEYS candidates), which always covers the legitimate work
|
|
366
|
+
// while still bounding the bounded-collision amplification. Chain length
|
|
367
|
+
// itself is capped (a delegation can't be deeper than a DNS name's label
|
|
368
|
+
// count, RFC 1035), so the scaled budget can't be inflated arbitrarily.
|
|
369
|
+
var MAX_COLLIDING_KEYS = 4; // allow:raw-byte-literal — same-tag DNSKEY candidates tried per RRSIG
|
|
370
|
+
var MAX_VALIDATIONS_PER_LINK = 8; // allow:raw-byte-literal — 2 RRSIGs/link x MAX_COLLIDING_KEYS; budget = links.length x this
|
|
371
|
+
var MAX_CHAIN_LINKS = 128; // allow:raw-byte-literal — max delegation depth (>= RFC 1035 max label count)
|
|
372
|
+
var MAX_DNSKEYS_PER_ZONE = 64; // allow:raw-byte-literal — DNSKEY RRset size cap per zone link
|
|
373
|
+
var MAX_DS_RECORDS = 16; // allow:raw-byte-literal — DS RRset size cap (parent-supplied)
|
|
344
374
|
|
|
345
375
|
// RFC 4648 §7 base32hex decode (no padding, case-insensitive) — the
|
|
346
376
|
// label encoding of an NSEC3 owner-name hash.
|
|
@@ -757,12 +787,31 @@ function _keysByTag(dnskeys, tag) {
|
|
|
757
787
|
// returning the key that validated. A wrong colliding key yields
|
|
758
788
|
// `dnssec/bad-signature` — that is not terminal, the next candidate is
|
|
759
789
|
// tried; any other error (expired, alg) is terminal. RFC 4035 §5.3.1.
|
|
760
|
-
function _verifyRrsetWithAnyKey(rrsetBase, rrsig, candidates, noKeyCode, noKeyMsg) {
|
|
790
|
+
function _verifyRrsetWithAnyKey(rrsetBase, rrsig, candidates, noKeyCode, noKeyMsg, budget) {
|
|
761
791
|
if (candidates.length === 0) throw new DnssecError(noKeyCode, noKeyMsg);
|
|
792
|
+
// KeyTrap (CVE-2023-50387): refuse an absurd same-tag fan-out outright —
|
|
793
|
+
// legitimate zones have 1-2 keys per tag; hundreds is an amplification
|
|
794
|
+
// attack, not a real collision.
|
|
795
|
+
if (candidates.length > MAX_COLLIDING_KEYS) {
|
|
796
|
+
throw new DnssecError("dnssec/too-many-colliding-keys",
|
|
797
|
+
"dnssec.verifyChain: " + candidates.length + " DNSKEYs share key tag " +
|
|
798
|
+
rrsig.keyTag + " (cap " + MAX_COLLIDING_KEYS +
|
|
799
|
+
") — refused as a KeyTrap (CVE-2023-50387) amplification vector");
|
|
800
|
+
}
|
|
762
801
|
var lastErr = null;
|
|
763
802
|
for (var i = 0; i < candidates.length; i++) {
|
|
764
803
|
var kp = _dnskeyParts(candidates[i]);
|
|
765
804
|
if (kp.algorithm !== rrsig.algorithm) { lastErr = new DnssecError("dnssec/alg-mismatch", "dnssec.verifyChain: candidate key algorithm does not match the RRSIG"); continue; }
|
|
805
|
+
// Per-response signature-validation budget — bound the total expensive
|
|
806
|
+
// pubkey verifies across the whole chain walk, not just per RRSIG.
|
|
807
|
+
if (budget) {
|
|
808
|
+
if (budget.remaining <= 0) {
|
|
809
|
+
throw new DnssecError("dnssec/validation-budget-exceeded",
|
|
810
|
+
"dnssec.verifyChain: per-response signature-validation budget " +
|
|
811
|
+
"exhausted — refused as a KeyTrap (CVE-2023-50387) amplification vector");
|
|
812
|
+
}
|
|
813
|
+
budget.remaining -= 1;
|
|
814
|
+
}
|
|
766
815
|
try {
|
|
767
816
|
verifyRrset(Object.assign({}, rrsetBase, { rrsig: rrsig, dnskey: { algorithm: kp.algorithm, publicKey: kp.publicKey } }));
|
|
768
817
|
return candidates[i];
|
|
@@ -795,6 +844,20 @@ function _verifyRrsetWithAnyKey(rrsetBase, rrsig, candidates, noKeyCode, noKeyMs
|
|
|
795
844
|
* passes to <code>verifyRrset</code> / <code>verifyDenial</code> for the
|
|
796
845
|
* actual answer.
|
|
797
846
|
*
|
|
847
|
+
* KeyTrap (CVE-2023-50387) amplification is bounded with non-configurable
|
|
848
|
+
* caps: at most 4 same-tag DNSKEY candidates are tried per RRSIG, at most
|
|
849
|
+
* 64 DNSKEYs per zone link and 16 DS records per delegation are accepted,
|
|
850
|
+
* the chain is at most 128 links deep, and the whole response is held to a
|
|
851
|
+
* signature-validation budget that scales with chain depth (so a
|
|
852
|
+
* legitimate deep delegation always fits while bounded collisions stay
|
|
853
|
+
* bounded). A hostile zone publishing many colliding keys / signatures is
|
|
854
|
+
* refused with <code>dnssec/too-many-colliding-keys</code> /
|
|
855
|
+
* <code>dnssec/too-many-dnskeys</code> / <code>dnssec/too-many-ds</code> /
|
|
856
|
+
* <code>dnssec/too-many-links</code> /
|
|
857
|
+
* <code>dnssec/validation-budget-exceeded</code> rather than driving
|
|
858
|
+
* O(keys x sigs) verifications. (NSEC3 iteration counts are separately
|
|
859
|
+
* capped at 150 per RFC 9276 / the CVE-2023-50868 fix.)
|
|
860
|
+
*
|
|
798
861
|
* @opts
|
|
799
862
|
* {
|
|
800
863
|
* links: [ { // ordered root-first
|
|
@@ -816,14 +879,35 @@ function verifyChain(opts) {
|
|
|
816
879
|
validateOpts.requireObject(opts, "dnssec.verifyChain", DnssecError);
|
|
817
880
|
validateOpts(opts, ["links", "trustAnchors", "at"], "dnssec.verifyChain");
|
|
818
881
|
if (!Array.isArray(opts.links) || opts.links.length === 0) throw new DnssecError("dnssec/bad-arg", "dnssec.verifyChain: opts.links must be a non-empty array");
|
|
882
|
+
// Cap delegation depth — a real chain can't be deeper than a DNS name's
|
|
883
|
+
// label count (RFC 1035), and the per-response validation budget below
|
|
884
|
+
// scales with this, so it must be bounded.
|
|
885
|
+
if (opts.links.length > MAX_CHAIN_LINKS) {
|
|
886
|
+
throw new DnssecError("dnssec/too-many-links",
|
|
887
|
+
"dnssec.verifyChain: " + opts.links.length + " chain links (cap " +
|
|
888
|
+
MAX_CHAIN_LINKS + ") — refused as an amplification vector");
|
|
889
|
+
}
|
|
819
890
|
var anchors = opts.trustAnchors !== undefined ? opts.trustAnchors : DEFAULT_ROOT_ANCHORS;
|
|
820
891
|
if (!Array.isArray(anchors) || anchors.length === 0) throw new DnssecError("dnssec/bad-arg", "dnssec.verifyChain: opts.trustAnchors must be a non-empty array");
|
|
821
892
|
|
|
893
|
+
// KeyTrap budget shared across every signature-validation in this
|
|
894
|
+
// response, scaled to the declared chain depth so a legitimate deep
|
|
895
|
+
// delegation (2N-1 verifies) always fits while bounded collisions stay
|
|
896
|
+
// bounded. Chain length is capped above, so this can't be inflated.
|
|
897
|
+
var budget = { remaining: opts.links.length * MAX_VALIDATIONS_PER_LINK };
|
|
898
|
+
|
|
822
899
|
var trustedKeys = null, path = [];
|
|
823
900
|
for (var i = 0; i < opts.links.length; i++) {
|
|
824
901
|
var link = opts.links[i];
|
|
825
902
|
if (!link || typeof link.zone !== "string" || link.zone === "") throw new DnssecError("dnssec/bad-link", "dnssec.verifyChain: links[" + i + "].zone is required");
|
|
826
903
|
if (!Array.isArray(link.dnskeys) || link.dnskeys.length === 0) throw new DnssecError("dnssec/bad-link", "dnssec.verifyChain: links[" + i + "].dnskeys must be a non-empty array");
|
|
904
|
+
// KeyTrap: bound the DNSKEY RRset size per zone so a giant key set
|
|
905
|
+
// can't blow up the key-tag scan / candidate fan-out.
|
|
906
|
+
if (link.dnskeys.length > MAX_DNSKEYS_PER_ZONE) {
|
|
907
|
+
throw new DnssecError("dnssec/too-many-dnskeys",
|
|
908
|
+
"dnssec.verifyChain: links[" + i + "] has " + link.dnskeys.length +
|
|
909
|
+
" DNSKEYs (cap " + MAX_DNSKEYS_PER_ZONE + ") — refused as a KeyTrap (CVE-2023-50387) amplification vector");
|
|
910
|
+
}
|
|
827
911
|
if (!link.dnskeyRrsig || typeof link.dnskeyRrsig !== "object") throw new DnssecError("dnssec/bad-link", "dnssec.verifyChain: links[" + i + "].dnskeyRrsig is required");
|
|
828
912
|
|
|
829
913
|
// 1. The DNSKEY RRset is self-signed by one of its own keys (trying
|
|
@@ -832,7 +916,8 @@ function verifyChain(opts) {
|
|
|
832
916
|
{ name: link.zone, type: "DNSKEY", rdatas: link.dnskeys, at: opts.at },
|
|
833
917
|
link.dnskeyRrsig,
|
|
834
918
|
_keysByTag(link.dnskeys, link.dnskeyRrsig.keyTag),
|
|
835
|
-
"dnssec/chain-no-signing-key", "dnssec.verifyChain: no DNSKEY in '" + link.zone + "' verifies the DNSKEY RRSIG"
|
|
919
|
+
"dnssec/chain-no-signing-key", "dnssec.verifyChain: no DNSKEY in '" + link.zone + "' verifies the DNSKEY RRSIG",
|
|
920
|
+
budget
|
|
836
921
|
);
|
|
837
922
|
|
|
838
923
|
// 2. Establish trust in the signing key.
|
|
@@ -851,11 +936,19 @@ function verifyChain(opts) {
|
|
|
851
936
|
if (!Array.isArray(link.dsRdatas) || link.dsRdatas.length === 0 || !link.dsRrsig || typeof link.dsRrsig !== "object") {
|
|
852
937
|
throw new DnssecError("dnssec/bad-link", "dnssec.verifyChain: links[" + i + "] needs dsRdatas + dsRrsig (DS served by the parent)");
|
|
853
938
|
}
|
|
939
|
+
// Bound the parent-supplied DS RRset — the DS-match loop below
|
|
940
|
+
// iterates it, and an oversize set is an amplification vector.
|
|
941
|
+
if (link.dsRdatas.length > MAX_DS_RECORDS) {
|
|
942
|
+
throw new DnssecError("dnssec/too-many-ds",
|
|
943
|
+
"dnssec.verifyChain: links[" + i + "] has " + link.dsRdatas.length +
|
|
944
|
+
" DS records (cap " + MAX_DS_RECORDS + ") — refused as an amplification vector");
|
|
945
|
+
}
|
|
854
946
|
_verifyRrsetWithAnyKey(
|
|
855
947
|
{ name: link.zone, type: "DS", rdatas: link.dsRdatas, at: opts.at },
|
|
856
948
|
link.dsRrsig,
|
|
857
949
|
_keysByTag(trustedKeys, link.dsRrsig.keyTag),
|
|
858
|
-
"dnssec/chain-no-parent-key", "dnssec.verifyChain: no trusted parent key verifies the DS RRSIG for '" + link.zone + "'"
|
|
950
|
+
"dnssec/chain-no-parent-key", "dnssec.verifyChain: no trusted parent key verifies the DS RRSIG for '" + link.zone + "'",
|
|
951
|
+
budget
|
|
859
952
|
);
|
|
860
953
|
var dsMatched = false;
|
|
861
954
|
for (var d = 0; d < link.dsRdatas.length; d++) {
|
package/lib/parsers/safe-toml.js
CHANGED
|
@@ -681,6 +681,14 @@ function parse(input, opts) {
|
|
|
681
681
|
"toml/redefine");
|
|
682
682
|
}
|
|
683
683
|
t = sub[sub.length - 1];
|
|
684
|
+
// The array's last element must itself be a table to descend
|
|
685
|
+
// into. A plain VALUE array (e.g. `a = [3]` then `[a.s]`) has a
|
|
686
|
+
// scalar last element — descending would set a property on a
|
|
687
|
+
// number and throw a raw TypeError; refuse it cleanly instead.
|
|
688
|
+
if (t === null || typeof t !== "object" || Array.isArray(t)) {
|
|
689
|
+
throw _err("cannot descend into '" + seg +
|
|
690
|
+
"' — it is a value array, not an array of tables", "toml/redefine");
|
|
691
|
+
}
|
|
684
692
|
continue;
|
|
685
693
|
}
|
|
686
694
|
if (typeof sub !== "object" || sub === null) {
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f4f503eb-ba54-47ab-8286-d7c3e006249e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-27T16:53:17.354Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.15",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.15",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.13.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|