@blamejs/core 0.13.7 → 0.13.9
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/README.md +1 -1
- package/lib/archive-gz.js +5 -3
- package/lib/archive-read.js +84 -35
- package/lib/archive-tar-read.js +86 -31
- package/lib/auth/jwt-external.js +6 -3
- package/lib/auth/jwt.js +2 -2
- package/lib/auth/oauth.js +2 -2
- package/lib/auth/saml.js +15 -12
- package/lib/calendar.js +1 -1
- package/lib/crypto-hpke.js +1 -1
- package/lib/crypto-oprf.js +2 -2
- package/lib/crypto.js +2 -2
- package/lib/framework-error.js +2 -1
- package/lib/guard-jwt.js +3 -2
- package/lib/guard-smtp-command.js +2 -2
- package/lib/mail-auth.js +2 -2
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-crypto-smime.js +7 -7
- package/lib/mail-crypto.js +1 -1
- package/lib/mail-dav.js +5 -4
- package/lib/mail-deploy.js +3 -2
- package/lib/mail-server-imap.js +1 -1
- package/lib/mail-server-jmap.js +1 -1
- package/lib/mail-server-managesieve.js +1 -1
- package/lib/mail-server-mx.js +4 -4
- package/lib/mail-server-submission.js +3 -3
- package/lib/mail-store.js +2 -2
- package/lib/mail.js +2 -2
- package/lib/network-tls.js +10 -7
- package/lib/safe-decompress.js +8 -6
- package/lib/safe-ical.js +12 -12
- package/lib/safe-mime.js +6 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-smtp.js +1 -1
- 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.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.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- v0.13.6 (2026-05-26) — **`b.ai.frontierModelProtocol` — California SB 53 frontier-AI obligations.** b.ai.frontierModelProtocol assesses a developer's obligations under California's Transparency in Frontier Artificial Intelligence Act — SB 53, Cal. Bus. & Prof. Code §22757.10, effective 2026-01-01 — from a model's training compute and the developer's revenue. It reports whether the model crosses the frontier threshold (more than 10^26 training FLOPs), whether the developer is a large frontier developer (prior-year revenue, with affiliates, above $500M), and the resulting obligations: every frontier developer must report critical safety incidents and publish a transparency report, and a large frontier developer must additionally publish an annual safety framework and disclose its catastrophic-risk assessment. Passing a candidate safety framework reports which required elements (risk identification, mitigation, governance, cybersecurity, standards alignment) are missing. b.ai.frontierModelProtocol.incidentReport validates a critical-incident type against the Act's four categories and computes the notification deadline to the California Office of Emergency Services — 15 days from discovery, or 24 hours when there is an imminent risk of death or serious physical injury. The ca-tfaia compliance posture was already in the catalog. **Added:** *`b.ai.frontierModelProtocol` — SB 53 threshold classification, obligations, and incident reporting* — `b.ai.frontierModelProtocol({ trainingFlops, annualRevenueUsd, framework? })` returns `isFrontierModel`, `isLargeFrontierDeveloper`, the `obligations` list, and (when a framework is supplied) its `frameworkGaps`. `b.ai.frontierModelProtocol.incidentReport({ type, discoveredAt, imminentRiskToLife? })` builds a critical-safety-incident report with the California OES recipient and a `dueAt` / `deadlineHours` computed from the 15-day (or 24-hour imminent-risk) statutory window; `type` must be one of the four categories in `INCIDENT_TYPES`. Throws `FrontierProtocolError` on malformed input.
|
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
228
228
|
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`)
|
|
229
229
|
- **CSV** — RFC 4180 with Excel formula-injection prevention (`b.csv`)
|
|
230
230
|
- **IDs + slugs** — RFC 9562 UUID v4 + v7 (`b.uuid`); URL-safe slugs (`b.slug`)
|
|
231
|
-
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
231
|
+
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); in-memory extraction with no disk write for read-only / serverless filesystems (`b.archive.read.zip(...).extractEntries()` / `.tar`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
232
232
|
- **Pagination + forms** — HMAC-signed cursor pagination (`b.pagination`); HTML form rendering + validation + CSRF (`b.forms`)
|
|
233
233
|
|
|
234
234
|
### Production
|
package/lib/archive-gz.js
CHANGED
|
@@ -47,9 +47,11 @@ function _isGzipMagic(buf) {
|
|
|
47
47
|
* `toAdapter(adapter)` / `digest()` — so gzip slots into the same
|
|
48
48
|
* downstream sinks (object-store + filesystem + http adapters).
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* `b.archive.tar().toGzip(adapter)` composes this primitive after
|
|
51
|
+
* materializing the tar bytes (the canonical `.tar.gz`). There is no
|
|
52
|
+
* `zip().toGzip()` — a ZIP is already DEFLATE-compressed per entry, so
|
|
53
|
+
* gzip-wrapping it would compress already-compressed data for no gain;
|
|
54
|
+
* gzip the uncompressed tar stream instead.
|
|
53
55
|
*
|
|
54
56
|
* @opts
|
|
55
57
|
* level: number, // 0-9, default 6 (zlib default).
|
package/lib/archive-read.js
CHANGED
|
@@ -546,21 +546,13 @@ function zip(adapter, opts) {
|
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
var
|
|
556
|
-
if (!nodeFs.existsSync(destination)) {
|
|
557
|
-
nodeFs.mkdirSync(destination, { recursive: true });
|
|
558
|
-
}
|
|
559
|
-
var loaded = await _loadCD();
|
|
560
|
-
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
561
|
-
// Compose b.guardArchive on the metadata pass — operators with a
|
|
562
|
-
// posture set declared via opts.guardProfile get the cascade.
|
|
563
|
-
var guardEntries = loaded.entries.map(function (e) {
|
|
549
|
+
// Run the b.guardArchive metadata cascade and refuse the whole archive on
|
|
550
|
+
// any critical issue. Shared by disk `extract` and in-memory `extractEntries`
|
|
551
|
+
// so both apply the identical posture-aware refusal. `auditAction` names the
|
|
552
|
+
// audit row for the refusal path.
|
|
553
|
+
function _assertGuardMetadata(loadedEntries, auditAction) {
|
|
554
|
+
if (opts.guardProfile === false) return;
|
|
555
|
+
var guardEntries = loadedEntries.map(function (e) {
|
|
564
556
|
return {
|
|
565
557
|
name: e.name,
|
|
566
558
|
size: e.uncompressedSize,
|
|
@@ -573,24 +565,80 @@ function zip(adapter, opts) {
|
|
|
573
565
|
attrs: { externalAttrs: e.externalAttrs },
|
|
574
566
|
};
|
|
575
567
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
568
|
+
var profile = opts.guardProfile || "balanced";
|
|
569
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
570
|
+
if (!guardResult || !Array.isArray(guardResult.issues) || guardResult.issues.length === 0) return;
|
|
571
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
572
|
+
if (critical.length === 0) return;
|
|
573
|
+
_emitAudit(opts, auditAction, "refused", {
|
|
574
|
+
entries: loadedEntries.length,
|
|
575
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
576
|
+
});
|
|
577
|
+
throw new ArchiveReadError("archive-read/guard-refused",
|
|
578
|
+
"extract refused — " + critical.length + " critical guard issue(s): " +
|
|
579
|
+
critical.map(function (i) { return i.ruleId + " (" + i.snippet + ")"; }).join("; "));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// In-memory extraction: yields each file entry's decompressed bytes WITHOUT
|
|
583
|
+
// writing to disk — for read-only / serverless filesystems. Applies the same
|
|
584
|
+
// bomb-policy, guard cascade, entry-type policy, and per-entry filename
|
|
585
|
+
// safety as `extract`; directory entries are skipped (no bytes). The realpath
|
|
586
|
+
// agreement check is disk-specific and intentionally omitted (no extraction
|
|
587
|
+
// root); the caller owns where, if anywhere, the bytes land.
|
|
588
|
+
async function* extractEntries(extractOpts) {
|
|
589
|
+
extractOpts = extractOpts || {};
|
|
590
|
+
var loaded = await _loadCD();
|
|
591
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
592
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extractEntries.refused");
|
|
593
|
+
var totalDecompressed = 0;
|
|
594
|
+
var yielded = 0;
|
|
595
|
+
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
596
|
+
var entry = loaded.entries[i];
|
|
597
|
+
if (entry.isEncrypted && !extractOpts.allowEncrypted) {
|
|
598
|
+
throw new ArchiveReadError("archive-read/encrypted-entry",
|
|
599
|
+
"entry " + JSON.stringify(entry.name) + " is encrypted — not decrypted on the in-memory path");
|
|
600
|
+
}
|
|
601
|
+
var typeRefusal = _enforceEntryTypePolicy(entry, entryTypePolicy);
|
|
602
|
+
if (typeRefusal) {
|
|
603
|
+
throw new ArchiveReadError("archive-read/entry-type-refused",
|
|
604
|
+
"entry " + JSON.stringify(entry.name) + " is a " + typeRefusal +
|
|
605
|
+
" — refused by entryTypePolicy");
|
|
606
|
+
}
|
|
607
|
+
if (_isDirectoryEntry(entry.name, entry.externalAttrs)) continue;
|
|
608
|
+
// Archive-level name threats (Zip-Slip traversal, etc.) are refused for
|
|
609
|
+
// the whole archive by the guardArchive cascade above. The caller owns
|
|
610
|
+
// final placement of the returned bytes; we deliberately do NOT apply the
|
|
611
|
+
// disk-write filename policy (shell-exec extensions / reserved names) here
|
|
612
|
+
// — nothing is written, and over-filtering would drop legitimate names.
|
|
613
|
+
var lfhResult = await _verifyLfhMatchesCd(adapter, entry);
|
|
614
|
+
var body = await _decompressEntry(adapter, entry, lfhResult.dataStart, bombPolicy);
|
|
615
|
+
totalDecompressed += body.length;
|
|
616
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
617
|
+
throw new ArchiveReadError("archive-read/total-too-large",
|
|
618
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
619
|
+
" exceeds maxTotalDecompressedBytes during extractEntries");
|
|
592
620
|
}
|
|
621
|
+
yielded += 1;
|
|
622
|
+
yield { name: entry.name, bytes: body, size: body.length };
|
|
623
|
+
}
|
|
624
|
+
_emitAudit(opts, "archive.read.extractEntries.completed", "success", { entries: yielded });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function extract(extractOpts) {
|
|
628
|
+
extractOpts = extractOpts || {};
|
|
629
|
+
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
630
|
+
throw new ArchiveReadError("archive-read/no-destination",
|
|
631
|
+
"extract: opts.destination must be a non-empty string (target directory)");
|
|
593
632
|
}
|
|
633
|
+
var destination = nodePath.resolve(extractOpts.destination);
|
|
634
|
+
if (!nodeFs.existsSync(destination)) {
|
|
635
|
+
nodeFs.mkdirSync(destination, { recursive: true });
|
|
636
|
+
}
|
|
637
|
+
var loaded = await _loadCD();
|
|
638
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
639
|
+
// Compose b.guardArchive on the metadata pass — operators with a
|
|
640
|
+
// posture set declared via opts.guardProfile get the cascade.
|
|
641
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extract.refused");
|
|
594
642
|
var written = [];
|
|
595
643
|
var bytesExtracted = 0;
|
|
596
644
|
var totalDecompressed = 0;
|
|
@@ -693,10 +741,11 @@ function zip(adapter, opts) {
|
|
|
693
741
|
}
|
|
694
742
|
|
|
695
743
|
return {
|
|
696
|
-
kind:
|
|
697
|
-
inspect:
|
|
698
|
-
entries:
|
|
699
|
-
extract:
|
|
744
|
+
kind: "zip-random-access",
|
|
745
|
+
inspect: inspect,
|
|
746
|
+
entries: entries,
|
|
747
|
+
extract: extract,
|
|
748
|
+
extractEntries: extractEntries,
|
|
700
749
|
};
|
|
701
750
|
}
|
|
702
751
|
|
package/lib/archive-tar-read.js
CHANGED
|
@@ -276,6 +276,87 @@ function tar(adapter, opts) {
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Shared b.guardArchive metadata cascade — disk `extract` + in-memory
|
|
280
|
+
// `extractEntries` refuse the whole archive identically on a critical issue.
|
|
281
|
+
function _assertGuardMetadata(entries, auditAction) {
|
|
282
|
+
if (opts.guardProfile === false) return;
|
|
283
|
+
var profile = opts.guardProfile || "balanced";
|
|
284
|
+
var guardEntries = entries.map(function (e) {
|
|
285
|
+
return {
|
|
286
|
+
name: e.name,
|
|
287
|
+
size: e.size,
|
|
288
|
+
compressedSize: e.size,
|
|
289
|
+
isSymlink: e.typeflag === TF_SYMLINK,
|
|
290
|
+
isHardlink: e.typeflag === TF_HARDLINK,
|
|
291
|
+
linkTarget: e.linkname,
|
|
292
|
+
isDirectory: e.typeflag === TF_DIRECTORY,
|
|
293
|
+
isEncrypted: false,
|
|
294
|
+
attrs: { mode: e.mode },
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
298
|
+
if (!guardResult || !Array.isArray(guardResult.issues)) return;
|
|
299
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
300
|
+
if (critical.length === 0) return;
|
|
301
|
+
_emitAudit(opts, auditAction, "refused", {
|
|
302
|
+
entries: entries.length,
|
|
303
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
304
|
+
});
|
|
305
|
+
throw new TarError("archive-tar/guard-refused",
|
|
306
|
+
"extract refused — " + critical.length + " critical guard issue(s)");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// In-memory extraction: yields each regular file entry's bytes without
|
|
310
|
+
// writing to disk (read-only / serverless filesystems). Same guard cascade,
|
|
311
|
+
// type-policy refusals, filename safety, and bomb cap as `extract`; directory
|
|
312
|
+
// and (validated) link entries carry no content and are not yielded.
|
|
313
|
+
async function* extractEntries(extractOpts) {
|
|
314
|
+
extractOpts = extractOpts || {};
|
|
315
|
+
var allowDangerous = extractOpts.allowDangerous || {};
|
|
316
|
+
var walked = await _walk();
|
|
317
|
+
var entries = walked.entries;
|
|
318
|
+
var bytes = walked.bytes;
|
|
319
|
+
_assertGuardMetadata(entries, "archive.read.tar.extractEntries.refused");
|
|
320
|
+
var totalDecompressed = 0;
|
|
321
|
+
var yielded = 0;
|
|
322
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
323
|
+
var entry = entries[i];
|
|
324
|
+
var type = _classifyTypeflag(entry.typeflag);
|
|
325
|
+
if (type === "device" || type === "fifo" || type === "socket") {
|
|
326
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
327
|
+
"entry " + JSON.stringify(entry.name) + " is a " + type + " — refused unconditionally");
|
|
328
|
+
}
|
|
329
|
+
if (type === "symlink" && !(allowDangerous.symlinks || entryTypePolicy.symlinks)) {
|
|
330
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
331
|
+
"entry " + JSON.stringify(entry.name) + " is a symlink — refused by entryTypePolicy");
|
|
332
|
+
}
|
|
333
|
+
if (type === "hardlink" && !(allowDangerous.hardlinks || entryTypePolicy.hardlinks)) {
|
|
334
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
335
|
+
"entry " + JSON.stringify(entry.name) + " is a hardlink — refused by entryTypePolicy");
|
|
336
|
+
}
|
|
337
|
+
// Directories + (now-permitted) links carry no content bytes.
|
|
338
|
+
if (type === "directory" || type === "symlink" || type === "hardlink") continue;
|
|
339
|
+
// Archive-level name threats are refused by the guardArchive cascade
|
|
340
|
+
// above; the caller owns placement of the returned bytes, so the
|
|
341
|
+
// disk-write filename policy is intentionally not applied here.
|
|
342
|
+
// COPY the slice — bytes.subarray/slice shares the full collected-archive
|
|
343
|
+
// backing store, so a caller retaining one entry would pin the whole
|
|
344
|
+
// archive in memory, defeating the serverless memory goal. Buffer.from
|
|
345
|
+
// gives the entry its own backing store (matching the ZIP path, whose
|
|
346
|
+
// _decompressEntry already returns a fresh buffer).
|
|
347
|
+
var body = Buffer.from(bytes.subarray(entry._bodyStart, entry._bodyStart + entry.size));
|
|
348
|
+
totalDecompressed += body.length;
|
|
349
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
350
|
+
throw new TarError("archive-tar/total-too-large",
|
|
351
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
352
|
+
" exceeds maxTotalDecompressedBytes during extractEntries");
|
|
353
|
+
}
|
|
354
|
+
yielded += 1;
|
|
355
|
+
yield { name: entry.name, bytes: body, size: body.length };
|
|
356
|
+
}
|
|
357
|
+
_emitAudit(opts, "archive.read.tar.extractEntries.completed", "success", { entries: yielded });
|
|
358
|
+
}
|
|
359
|
+
|
|
279
360
|
async function extract(extractOpts) {
|
|
280
361
|
extractOpts = extractOpts || {};
|
|
281
362
|
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
@@ -290,34 +371,7 @@ function tar(adapter, opts) {
|
|
|
290
371
|
var walked = await _walk();
|
|
291
372
|
var entries = walked.entries;
|
|
292
373
|
var bytes = walked.bytes;
|
|
293
|
-
|
|
294
|
-
var profile = opts.guardProfile || "balanced";
|
|
295
|
-
var guardEntries = entries.map(function (e) {
|
|
296
|
-
return {
|
|
297
|
-
name: e.name,
|
|
298
|
-
size: e.size,
|
|
299
|
-
compressedSize: e.size,
|
|
300
|
-
isSymlink: e.typeflag === TF_SYMLINK,
|
|
301
|
-
isHardlink: e.typeflag === TF_HARDLINK,
|
|
302
|
-
linkTarget: e.linkname,
|
|
303
|
-
isDirectory: e.typeflag === TF_DIRECTORY,
|
|
304
|
-
isEncrypted: false,
|
|
305
|
-
attrs: { mode: e.mode },
|
|
306
|
-
};
|
|
307
|
-
});
|
|
308
|
-
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
309
|
-
if (guardResult && Array.isArray(guardResult.issues)) {
|
|
310
|
-
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
311
|
-
if (critical.length > 0) {
|
|
312
|
-
_emitAudit(opts, "archive.read.tar.extract.refused", "refused", {
|
|
313
|
-
entries: entries.length,
|
|
314
|
-
issues: critical.map(function (i) { return i.ruleId; }),
|
|
315
|
-
});
|
|
316
|
-
throw new TarError("archive-tar/guard-refused",
|
|
317
|
-
"extract refused — " + critical.length + " critical guard issue(s)");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
374
|
+
_assertGuardMetadata(entries, "archive.read.tar.extract.refused");
|
|
321
375
|
var written = [];
|
|
322
376
|
var bytesExtracted = 0;
|
|
323
377
|
var totalDecompressed = 0;
|
|
@@ -405,9 +459,10 @@ function tar(adapter, opts) {
|
|
|
405
459
|
}
|
|
406
460
|
|
|
407
461
|
return {
|
|
408
|
-
kind:
|
|
409
|
-
inspect:
|
|
410
|
-
extract:
|
|
462
|
+
kind: "tar-reader",
|
|
463
|
+
inspect: inspect,
|
|
464
|
+
extract: extract,
|
|
465
|
+
extractEntries: extractEntries,
|
|
411
466
|
};
|
|
412
467
|
}
|
|
413
468
|
|
package/lib/auth/jwt-external.js
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Defenses against the well-known JWT pitfalls:
|
|
30
30
|
*
|
|
31
|
-
* - alg confusion (CVE-2024-54150 / CVE-
|
|
32
|
-
*
|
|
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
|
|
@@ -480,7 +480,10 @@ async function verifyExternal(token, opts) {
|
|
|
480
480
|
// weak iss validation. Constant-time compare defeats prefix-timing
|
|
481
481
|
// narrowing; emit a DISTINCT audit event (separate from sig-verify-
|
|
482
482
|
// fail) so detection signals lights up on the cross-realm shape
|
|
483
|
-
// independently of generic verification failures.
|
|
483
|
+
// independently of generic verification failures. The `typeof ... !==
|
|
484
|
+
// "string"` guard also rejects an array-valued iss (CVE-2025-30144,
|
|
485
|
+
// fast-jwt — an iss array `["attacker", "valid"]` passed an any-match
|
|
486
|
+
// check); only a single string iss is accepted.
|
|
484
487
|
if (typeof payload.iss !== "string" ||
|
|
485
488
|
!_issuerMatches(payload.iss, opts.issuer)) {
|
|
486
489
|
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 (
|
|
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
|
@@ -1344,8 +1344,8 @@ function create(opts) {
|
|
|
1344
1344
|
}
|
|
1345
1345
|
var iss = u.searchParams.get("iss");
|
|
1346
1346
|
var sid = u.searchParams.get("sid");
|
|
1347
|
-
//
|
|
1348
|
-
// present (defends against an attacker-controlled IdP forging a
|
|
1347
|
+
// OpenID Connect Front-Channel Logout 1.0 §3: `iss` MUST match the
|
|
1348
|
+
// configured issuer when present (defends against an attacker-controlled IdP forging a
|
|
1349
1349
|
// logout for a session at a different IdP). `sid` is required
|
|
1350
1350
|
// when the RP registered with frontchannel_logout_session_required=true;
|
|
1351
1351
|
// we surface it either way and let the operator decide.
|
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;
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
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:
|
|
1535
|
-
//
|
|
1536
|
-
// CBC mode under XMLEnc is exploitable without per-
|
|
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
|
|
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
|
|
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
|
-
"(
|
|
1716
|
+
"(XMLEnc CBC padding-oracle class, Jager & Somorovsky CCS 2011).");
|
|
1714
1717
|
}
|
|
1715
1718
|
return clearBytes.toString("utf8");
|
|
1716
1719
|
}
|
package/lib/calendar.js
CHANGED
|
@@ -515,7 +515,7 @@ function toIcal(jsCal, opts) {
|
|
|
515
515
|
* timestamps in the operator's `[from, to]` window. Returns an array
|
|
516
516
|
* of ISO 8601 UTC strings (`yyyy-mm-ddTHH:MM:SSZ`). Bounded by
|
|
517
517
|
* `MAX_EXPAND_INSTANCES` (4096) + `MAX_EXPAND_SPAN_MS` (10 years) to
|
|
518
|
-
* defend against
|
|
518
|
+
* defend against the RRULE recurrence-bomb expansion class.
|
|
519
519
|
*
|
|
520
520
|
* v1 supports FREQ=DAILY/WEEKLY/MONTHLY/YEARLY with INTERVAL, COUNT,
|
|
521
521
|
* UNTIL. BYDAY / BYMONTH / BYMONTHDAY / BYWEEKNO / BYYEARDAY /
|
package/lib/crypto-hpke.js
CHANGED
|
@@ -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
|
package/lib/crypto-oprf.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/lib/framework-error.js
CHANGED
|
@@ -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
|
|
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
|
|
20
|
-
*
|
|
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)
|
|
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
|
|
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.
|
|
1862
|
-
//
|
|
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")) ||
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -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 (
|
|
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) {
|
package/lib/mail-crypto-smime.js
CHANGED
|
@@ -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 (
|
|
25
|
-
*
|
|
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
|
-
* -
|
|
91
|
-
*
|
|
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 —
|
|
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
|
-
"(
|
|
711
|
+
"(SHAttered SHA-1 collision; RFC 8551 §2.5). Acceptable hashes: " + ALLOWED_HASHES.join(", "));
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
|
package/lib/mail-crypto.js
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
*
|
|
63
63
|
* CVE citations:
|
|
64
64
|
* - CVE-2017-17688 / CVE-2017-17689 (EFAIL)
|
|
65
|
-
* -
|
|
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
|
|
116
|
-
*
|
|
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
|
|
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
|
|
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, {
|
package/lib/mail-deploy.js
CHANGED
|
@@ -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)
|
|
531
|
-
// the §5.2 community ceiling (receivers commonly cap
|
|
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
|
package/lib/mail-server-imap.js
CHANGED
|
@@ -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
|
|
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-
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -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
|
|
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
|
package/lib/mail-server-mx.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* ## Defenses baked in
|
|
27
27
|
*
|
|
28
|
-
* - **SMTP smuggling** (CVE-2023-51764 / CVE-
|
|
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 /
|
|
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
|
|
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 /
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
package/lib/network-tls.js
CHANGED
|
@@ -462,12 +462,15 @@ function applyToContext(opts) {
|
|
|
462
462
|
// setKeyShares(["X25519MLKEM768", "X25519"]) → string[] (after)
|
|
463
463
|
// resetKeyShares() → restores default
|
|
464
464
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
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
|
|
470
|
-
// SecP256r1MLKEM768 — codepoint 0x11EB
|
|
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 —
|
|
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` (
|
|
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)) {
|
package/lib/safe-decompress.js
CHANGED
|
@@ -54,10 +54,12 @@
|
|
|
54
54
|
* bytes ever cross the audit boundary on the bomb-class path.
|
|
55
55
|
*
|
|
56
56
|
* Threat model:
|
|
57
|
-
* - **
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
* - [
|
|
77
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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 —
|
|
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
|
-
" (
|
|
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 + " (
|
|
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 —
|
|
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 + " (
|
|
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 + " (
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
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:8b6b1445-7522-46ce-ab72-88ef073536dd",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-27T07:12:04.477Z",
|
|
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.9",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.9",
|
|
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.9",
|
|
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.9",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|