@blamejs/blamejs-shop 0.1.37 → 0.2.0
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 +6 -0
- package/lib/admin.js +205 -42
- package/lib/asset-manifest.json +5 -5
- package/lib/storefront.js +428 -51
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +12 -0
- package/lib/vendor/blamejs/README.md +2 -2
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
- package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
- package/lib/vendor/blamejs/lib/archive-read.js +101 -35
- package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
- package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
- package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
- package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +2 -3
- package/lib/vendor/blamejs/lib/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- package/lib/vendor/blamejs/lib/cose.js +4 -3
- package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
- package/lib/vendor/blamejs/lib/crypto.js +2 -2
- package/lib/vendor/blamejs/lib/framework-error.js +2 -1
- package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
- package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
- package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
- package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- package/lib/vendor/blamejs/lib/mdoc.js +14 -14
- package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
- package/lib/vendor/blamejs/lib/network-tls.js +10 -7
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
- package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
- package/package.json +1 -1
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.12",
|
|
7
|
+
"tag": "v0.13.12",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,18 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
- v0.13.11 (2026-05-27) — **Test-suite reliability: replaced fixed-delay waits in the rate-limiter and scheduler suites with condition polling.** No runtime behaviour changes. The rate-limiter, scheduler, and websocket-channel test suites waited for asynchronous work to settle by draining a fixed number of event-loop ticks before asserting. Under heavily parallel CI that budget was occasionally too short, so an assertion read state before the async work (a cluster-backend counter update, a scheduler tick-claim) had landed — an intermittent failure unrelated to the code under test. Those waits now poll the observable condition (helpers.waitUntil) and exit as soon as it holds, with a generous upper bound, so they pass quickly on fast machines and reliably under load. A build gate is added so the fixed-tick-drain shape cannot be reintroduced. **Fixed:** *Flaky fixed-budget waits in the rate-limiter / scheduler / sandbox test suites made contention-tolerant* — The rate-limit-cluster and scheduler-exactly-once suites drained a fixed count of event-loop ticks before asserting on asynchronously-updated state; under contended CI the budget could expire before the work settled, producing intermittent failures. They now wait on the actual observable condition (a written response, a settled counter). The sandbox suite's success-path cases gave the worker a 5 s execution budget that cold worker-thread startup under heavily parallel Windows CI could just exceed; those are raised to the framework's 10 s ceiling. Affects test code only — no change to shipped framework behaviour. The unused tick-drain helper in the websocket-channel suite was removed. **Detectors:** *Build gate rejects the fixed-tick-drain wait shape in tests* — A new test-suite lint rule flags the counted microtask/tick-drain idiom (reassigning a promise to its own `.then()` in a loop to wait a fixed number of ticks), the sibling of the existing fixed-`setTimeout`-sleep rule. A single event-loop yield is unaffected; only the drain-as-wait shape is rejected, directing the wait to condition polling instead.
|
|
14
|
+
|
|
15
|
+
- v0.13.10 (2026-05-27) — **Documented-but-inert options wired up, a non-existent CVE reference removed, and a silent iCalendar cap-bypass fixed.** A sweep for places where a documented option or citation did not match what the code does. The most operator-relevant fix: b.calendar.fromIcal documented a safeIcalOpts option that forwards parser caps (byte size, RRULE limits, nesting depth) to b.safeIcal.parse, but the value was never forwarded — so an operator who set tight caps through it got the default profile instead, silently. That is corrected; the nested options now reach the parser. b.archive.read.zip documented an AbortSignal option that was never honored; it now aborts the read at the entry boundary. b.auth.fal documented a bearerOnly alias that had no effect; it now forces the no-proof-of-possession path and refuses the contradictory combination of bearerOnly:true with a holder-of-key binding. Separately, the auth verification paths cited CVE-2026-23993 (13 places) for the "reject an unknown alg before key lookup" guard — that CVE id does not exist (the registry has no record of it); the citation is replaced with the weakness class (CWE-347 / CWE-757) and the real, verifiable neighboring CVEs. The circuit-breaker error-code note that promised a rename "in v0.10" is corrected to the actual plan (v1.0), and the build gate that catches overdue version promises now also catches two-part version numbers. **Changed:** *`b.auth.fal` `bearerOnly` is now a real alias and refuses contradictions* — `bearerOnly: true` now forces the no-proof-of-possession path (equivalent to `hokBinding: null`), as documented. Passing `bearerOnly: true` together with a non-null `hokBinding` is a contradictory assurance request and is now refused at the call rather than silently resolved one way. **Fixed:** *`b.calendar.fromIcal` now forwards `safeIcalOpts` to the parser* — The documented `safeIcalOpts` option (parser caps: max bytes, RRULE COUNT/BYxxx limits, nesting depth) was not being passed to `b.safeIcal.parse` — when supplied under the documented nested key it was silently ignored and the parser ran with its default profile. Both forms now reach the parser: the documented nested `{ safeIcalOpts: { ... } }` and the top-level `{ profile, ... }` that earlier releases accepted, with the nested form winning on conflict. No caller regresses. · *`b.archive.read.zip` honors the documented `signal` (AbortSignal)* — The `signal` option was documented but never read. A large or slow archive read can now be aborted cooperatively — the reader checks the signal at each entry boundary (`inspect`, `entries`, `extractEntries`, `extract`) and rejects with an `archive-read/aborted` error. · *Removed a non-existent CVE reference from the JWT/JWE verification paths* — The "reject an unknown/unsupported `alg` before any key lookup" guard in `b.auth.jwt.verifyExternal`, `b.auth.oauth.verifyIdToken`, `b.auth.oid4vci`, and `b.auth.sd-jwt-vc` cited a CVE id that the registry has no record of. The behaviour is unchanged; the citation is now the weakness class it defends (CWE-347 improper signature verification / CWE-757 algorithm downgrade) alongside the real, verifiable alg-confusion / JWE-bypass CVEs already cited beside it. **Detectors:** *Overdue-version-promise gate now catches two-part version numbers* — The build gate that flags a deferral whose promised landing version has already shipped previously matched only three-part versions (`vN.N.N`); a two-part promise (`vN.N`) slipped past it. It now matches both. The `b.circuitBreaker` `CIRCUIT_OPEN` error-code note that pointed at a passed version is corrected to its actual plan (rename at v1.0, with a deprecation warning a minor ahead).
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
11
23
|
- 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.
|
|
12
24
|
|
|
13
25
|
- v0.13.5 (2026-05-26) — **`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit.** b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog. **Added:** *`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios* — `b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `"selection"` (group entries `{ selected, total }`) or `"scoring"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input.
|
|
@@ -169,7 +169,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
169
169
|
- **Mail (outbound)** — multipart + attachments + DKIM + calendar invites; bounce intake (`b.mail`, `b.mailBounce`)
|
|
170
170
|
- **Mail (outbound delivery)** — turnkey MX-lookup → MTA-STS-fetch → DANE-TLSA → REQUIRETLS handshake → SMTP wire layer → RFC 3464 DSN-on-permanent-failure → deferred-retry scheduling, all wired once (`b.mail.send.deliver`)
|
|
171
171
|
- **Mail (inbound auth)** — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`)
|
|
172
|
-
- **Mail server listeners** — RFC 5321 MX inbound (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
|
|
172
|
+
- **Mail server listeners** — RFC 5321 MX inbound with connection-level gate cascade (HELO identity / DNS blocklist / greylisting) (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
|
|
173
173
|
- **JMAP EmailSubmission reference** — composes `b.mail.send.deliver` to land the RFC 8621 §7.5 surface end-to-end (`b.mail.server.jmap.emailSubmissionSetHandler`)
|
|
174
174
|
- **Mail crypto** — PQC-first S/MIME via CMS (`b.mail.crypto.cms`) + OpenPGP encrypt/decrypt + WKD key discovery with IDN-homograph defense (`b.mail.crypto.pgp`)
|
|
175
175
|
- **Mail-stack agent** — multi-threaded worker pool + queue dispatch + sealed mail-store backed by SQLite FTS5 (`b.mail.agent`, `b.mailStore`)
|
|
@@ -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
|
|
@@ -332,6 +332,7 @@ This is the minimum-viable security posture for a production deployment. The fra
|
|
|
332
332
|
- [ ] For routes that accept YAML (config uploads, CI/CD pipelines, infra-as-code, document-import flows — ANY operator-supplied YAML the server parses): `b.guardYaml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound YAML bodies that don't go through those primitives, wire `b.guardYaml.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses deserialization-tag RCE (defends CVE-2026-24009 Docling/PyYAML, CVE-2022-1471 SnakeYAML, CVE-2017-18342 PyYAML class), billion-laughs alias recursion (CVE-2026-27807 MarkUs class), Norway-problem implicit booleans, multi-document streams, leading-zero octals, duplicate keys, merge-key anchor-chains, bidi/null/control chars. Unlike JSON, YAML's threat surface includes language-specific deserialization triggers — `!!python/object/new:...` / `!!java.util.HashMap` / `!!ruby/object` etc. — which the source-level scan catches before any downstream parser (PyYAML / SnakeYAML / js-yaml) sees them
|
|
333
333
|
- [ ] For routes that accept JSON bodies (REST APIs / webhook receivers / config uploads — ANY operator-supplied JSON the server parses): `b.guardJson.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound JSON request bodies that don't go through those primitives, wire `b.guardJson.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses prototype pollution at source level (catches `__proto__` / `constructor` / `prototype` keys before any parser sees the input — defends CVE-2025-55182 React Server Functions RCE class), duplicate keys (RFC 8259 SHOULD-unique smuggling), NaN/Infinity, comments, JSON5 syntax, BOM, bidi/null/control chars, numeric precision-loss, depth + breadth + array-length + string-length caps. Pair with `topLevelKeyAllowlist: [...]` for routes with a known shape so unauthorized keys refuse before validation
|
|
334
334
|
- [ ] For routes that accept email (inbound webhooks from mail providers, .eml uploads, mailbox imports, message-archival flows, customer-support-ticket-by-email — ANY operator-supplied RFC 822/5322 message the server processes): `b.guardEmail.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.17. For inbound message bytes that don't go through those primitives, wire `b.guardEmail.validateMessage(bytes, { profile: "strict" })` BEFORE the parser sees the message — strict refuses SMTP smuggling (bare CR / bare LF outside CRLF pairs combined with embedded SMTP verbs `MAIL FROM`/`RCPT TO`/`DATA`/`EHLO`/`HELO`/`RSET`/`QUIT` — defends CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET System.Net.Mail class), CRLF header injection in single-line headers (defends From/Bcc/body smuggling), IDN homograph mixed-script domains in address-bearing headers (Cyrillic / Greek / Armenian / Cherokee codepoints overlapping Latin — operator opts in to legitimate non-Latin via `allowedScripts: ["latin", "cyrillic"]`), Punycode `xn--` labels, display-name spoofing (`"support@apple.com" <attacker@evil>` — display contains @-address that doesn't match envelope domain), IP-literal addresses (`user@[1.2.3.4]` — bypasses DNS/DMARC alignment), RFC 5322 comment syntax in addresses, multiple @ characters, RFC 5321 length caps (local-part 64 / domain 255 / address 320), RFC 5322 line cap (998), BOM injection, bidi/null/control chars in addresses + headers. For per-address validation outside a full message context (form-submitted email, signup, MX-host validation), wire `b.guardEmail.validateAddress(addr, { profile: "strict" })`. Pair with operator's DMARC / SPF / DKIM verifier for envelope-alignment checks — guardEmail is the source-level gate, not the authentication-result interpreter
|
|
335
|
+
- [ ] For an inbound `b.mail.server.mx` listener, wire the connection-level gate cascade so the anti-abuse controls actually run: `helo: b.mail.helo` (FCrDNS / HELO-shape / self-name spoofing → 550), `rbl: b.mail.rbl.create({ providers: [...] })` (connecting-IP DNS blocklist → 554, evaluated once per connection), and `greylist: b.mail.greylist.create({ store })` (first-seen (ip, sender, recipient) → 450 tempfail). A gate you don't wire is skipped, not synthesized — so an unwired RBL is no protection, not a default-allow surprise. SPF/DKIM/DMARC alignment is performed on the delivered message via the agent handoff until the inbound-authentication pipeline lands
|
|
335
336
|
- [ ] For routes that accept markdown (rich-text editors, comment systems, README rendering, documentation submission, GitHub-style wikis, mail-rendered markdown, document-import flows — ANY operator-supplied markdown the server renders): `b.guardMarkdown.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.16. For inbound markdown bodies that don't go through those primitives, wire `b.guardMarkdown.validate(body, { profile: "strict" })` BEFORE passing the source to any markdown renderer (marked / markdown-it / commonmark / remark / parsedown — all of them) — strict refuses dangerous URL schemes in inline links + images + autolinks + reference-link definitions (defends CVE-2025-9540 Markup Markdown class, CVE-2025-24981 MDC class, NuGetGallery GHSA-gwjh-c548-f787, Joplin GHSA-hff8-hjwv-j9q7), whitespace-tolerant dangerous-tag matching (`<script\n>` / `<script\t>` — defends CVE-2026-30838 CommonMark DisallowedRawHtml bypass class), HTML-entity scheme bypass (`javascript:` / `javascript:` decoded BEFORE scheme matching), reference-link smuggling (`[label]: javascript:...`), front-matter YAML/TOML blocks, HTML comments, code-fence language injection (language tag containing `<>"' `` blocks attribute breakout), catastrophic emphasis runs (CVE-2025-6493 CodeMirror Markdown class, CVE-2025-7969 markdown-it class), inline DOCTYPE, bidi/null/control chars, total-bytes + line + link + image + autolink + ref-def + list-depth + blockquote-depth caps. **Layer with `b.guardHtml`**: source-level guardMarkdown then render then output-level guardHtml together close the residual bypass surface that either alone misses (markdown engines surprise; sanitizers also surprise — defense in depth)
|
|
336
337
|
- [ ] For routes that accept XML (SOAP endpoints, sitemap submissions, RSS / Atom feeds, OAI-PMH harvesters, SAML / WS-Federation receivers, document-import flows — ANY operator-supplied XML the server parses): `b.guardXml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.15. For inbound XML bodies that don't go through those primitives, wire `b.guardXml.validate(body, { profile: "strict" })` before passing the document to any XML parser — strict refuses DOCTYPE declarations unconditionally (XXE + billion-laughs vector — defends CVE-2026-24400 AssertJ class, CVE-2024-8176 libexpat recursive-entity stack-overflow class), `<!ENTITY>` declarations including parameter entities (out-of-band exfiltration vector), external entity references (SYSTEM / PUBLIC with file:// / http:// / ftp:// schemes — local file read + SSRF), `<xi:include>` remote inclusion (CVE-2024-25062 libxml2 use-after-free class), `xsi:schemaLocation` operator-controlled schema fetch, processing instructions (`<?xml-stylesheet ?>` CSS-injection vector), CDATA sections (often used to hide payloads from naive scanners), XML signature wrapping (xmldsig surface), bidi/null/control chars in element text + attribute values, and applies depth + element + per-attribute-value caps. DOCTYPE remains refused at every profile level (strict / balanced / permissive) because billion-laughs is universal. Operators integrating with legacy SOAP that requires DTDs must instead route through a separately-firewalled XML processor with explicit allowlist — the gate has no knob to relax DOCTYPE
|
|
337
338
|
- [ ] **For ZIP-shaped uploads specifically**, reach for `b.safeArchive.extract({ source, destination, guardProfile: "strict" })` — the one-liner composes `b.archive.read.zip`'s random-access reader (LFH/CD skew defense + CD-walk validation), `b.guardArchive.zipBombPolicy` defaults (per-entry + per-archive + ratio caps), `b.guardArchive.entryTypePolicy` defaults (symlink / hardlink / device / fifo / socket entries refused), and `b.guardFilename.verifyExtractionPath`'s dual-check (string-normalize + `fs.realpath`-agreement; refuses pre-resolve names exceeding PATH_MAX=4096 to defend the CVE-2025-4517 TOCTOU class). The fs-coupled realpath check is the depth above `b.guardArchive.checkExtractionPath`'s portable string-only gate; operators with their own extract loop call both. Default refuses ZIP encrypted entries (v0.12.10/11 add the encryption read paths). For tar / gzip / 7z / rar / zstd, the read-side primitives land in v0.12.8 / v0.12.9; until then use the legacy guard-only path below
|
|
@@ -187,9 +187,8 @@ function chatbot(session, opts) {
|
|
|
187
187
|
* machine-readable manner. This primitive returns the disclosure
|
|
188
188
|
* payload (visible label + structured metadata) the operator wires
|
|
189
189
|
* into the encoder / response pipeline. C2PA manifest emission is
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* adapter consumes when it lands.
|
|
190
|
+
* handled by `b.contentCredentials`; this primitive supplies the label
|
|
191
|
+
* markup and the metadata schema that the C2PA adapter consumes.
|
|
193
192
|
*
|
|
194
193
|
* @opts
|
|
195
194
|
* contentType: "image" | "audio" | "video" | "text", // required
|
|
@@ -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).
|
|
@@ -503,6 +503,18 @@ function zip(adapter, opts) {
|
|
|
503
503
|
var entryTypePolicy = _normalizeEntryTypePolicy(opts.entryTypePolicy);
|
|
504
504
|
var cdCache = null;
|
|
505
505
|
|
|
506
|
+
// Cooperative cancellation — operators pass an AbortSignal to bound a
|
|
507
|
+
// large/slow archive read. Checked between entries (the natural yield
|
|
508
|
+
// point); a long single-entry decompress is already bounded by the
|
|
509
|
+
// bomb policy.
|
|
510
|
+
function _throwIfAborted() {
|
|
511
|
+
if (opts.signal && opts.signal.aborted) {
|
|
512
|
+
throw new ArchiveReadError("archive-read/aborted",
|
|
513
|
+
"archive read aborted via opts.signal" +
|
|
514
|
+
(opts.signal.reason !== undefined ? ": " + String(opts.signal.reason) : ""));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
506
518
|
async function _loadCD() {
|
|
507
519
|
if (cdCache) return cdCache;
|
|
508
520
|
var eocd = await _locateEocd(adapter);
|
|
@@ -512,6 +524,7 @@ function zip(adapter, opts) {
|
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
async function inspect() {
|
|
527
|
+
_throwIfAborted();
|
|
515
528
|
var loaded = await _loadCD();
|
|
516
529
|
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
517
530
|
_emitAudit(opts, "archive.read.inspect", "success", {
|
|
@@ -539,28 +552,22 @@ function zip(adapter, opts) {
|
|
|
539
552
|
}
|
|
540
553
|
|
|
541
554
|
async function* entries() {
|
|
555
|
+
_throwIfAborted();
|
|
542
556
|
var loaded = await _loadCD();
|
|
543
557
|
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
544
558
|
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
559
|
+
_throwIfAborted();
|
|
545
560
|
yield loaded.entries[i];
|
|
546
561
|
}
|
|
547
562
|
}
|
|
548
563
|
|
|
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) {
|
|
564
|
+
// Run the b.guardArchive metadata cascade and refuse the whole archive on
|
|
565
|
+
// any critical issue. Shared by disk `extract` and in-memory `extractEntries`
|
|
566
|
+
// so both apply the identical posture-aware refusal. `auditAction` names the
|
|
567
|
+
// audit row for the refusal path.
|
|
568
|
+
function _assertGuardMetadata(loadedEntries, auditAction) {
|
|
569
|
+
if (opts.guardProfile === false) return;
|
|
570
|
+
var guardEntries = loadedEntries.map(function (e) {
|
|
564
571
|
return {
|
|
565
572
|
name: e.name,
|
|
566
573
|
size: e.uncompressedSize,
|
|
@@ -573,29 +580,87 @@ function zip(adapter, opts) {
|
|
|
573
580
|
attrs: { externalAttrs: e.externalAttrs },
|
|
574
581
|
};
|
|
575
582
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
583
|
+
var profile = opts.guardProfile || "balanced";
|
|
584
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
585
|
+
if (!guardResult || !Array.isArray(guardResult.issues) || guardResult.issues.length === 0) return;
|
|
586
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
587
|
+
if (critical.length === 0) return;
|
|
588
|
+
_emitAudit(opts, auditAction, "refused", {
|
|
589
|
+
entries: loadedEntries.length,
|
|
590
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
591
|
+
});
|
|
592
|
+
throw new ArchiveReadError("archive-read/guard-refused",
|
|
593
|
+
"extract refused — " + critical.length + " critical guard issue(s): " +
|
|
594
|
+
critical.map(function (i) { return i.ruleId + " (" + i.snippet + ")"; }).join("; "));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// In-memory extraction: yields each file entry's decompressed bytes WITHOUT
|
|
598
|
+
// writing to disk — for read-only / serverless filesystems. Applies the same
|
|
599
|
+
// bomb-policy, guard cascade, entry-type policy, and per-entry filename
|
|
600
|
+
// safety as `extract`; directory entries are skipped (no bytes). The realpath
|
|
601
|
+
// agreement check is disk-specific and intentionally omitted (no extraction
|
|
602
|
+
// root); the caller owns where, if anywhere, the bytes land.
|
|
603
|
+
async function* extractEntries(extractOpts) {
|
|
604
|
+
extractOpts = extractOpts || {};
|
|
605
|
+
var loaded = await _loadCD();
|
|
606
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
607
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extractEntries.refused");
|
|
608
|
+
var totalDecompressed = 0;
|
|
609
|
+
var yielded = 0;
|
|
610
|
+
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
611
|
+
_throwIfAborted();
|
|
612
|
+
var entry = loaded.entries[i];
|
|
613
|
+
if (entry.isEncrypted && !extractOpts.allowEncrypted) {
|
|
614
|
+
throw new ArchiveReadError("archive-read/encrypted-entry",
|
|
615
|
+
"entry " + JSON.stringify(entry.name) + " is encrypted — not decrypted on the in-memory path");
|
|
616
|
+
}
|
|
617
|
+
var typeRefusal = _enforceEntryTypePolicy(entry, entryTypePolicy);
|
|
618
|
+
if (typeRefusal) {
|
|
619
|
+
throw new ArchiveReadError("archive-read/entry-type-refused",
|
|
620
|
+
"entry " + JSON.stringify(entry.name) + " is a " + typeRefusal +
|
|
621
|
+
" — refused by entryTypePolicy");
|
|
592
622
|
}
|
|
623
|
+
if (_isDirectoryEntry(entry.name, entry.externalAttrs)) continue;
|
|
624
|
+
// Archive-level name threats (Zip-Slip traversal, etc.) are refused for
|
|
625
|
+
// the whole archive by the guardArchive cascade above. The caller owns
|
|
626
|
+
// final placement of the returned bytes; we deliberately do NOT apply the
|
|
627
|
+
// disk-write filename policy (shell-exec extensions / reserved names) here
|
|
628
|
+
// — nothing is written, and over-filtering would drop legitimate names.
|
|
629
|
+
var lfhResult = await _verifyLfhMatchesCd(adapter, entry);
|
|
630
|
+
var body = await _decompressEntry(adapter, entry, lfhResult.dataStart, bombPolicy);
|
|
631
|
+
totalDecompressed += body.length;
|
|
632
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
633
|
+
throw new ArchiveReadError("archive-read/total-too-large",
|
|
634
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
635
|
+
" exceeds maxTotalDecompressedBytes during extractEntries");
|
|
636
|
+
}
|
|
637
|
+
yielded += 1;
|
|
638
|
+
yield { name: entry.name, bytes: body, size: body.length };
|
|
639
|
+
}
|
|
640
|
+
_emitAudit(opts, "archive.read.extractEntries.completed", "success", { entries: yielded });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function extract(extractOpts) {
|
|
644
|
+
extractOpts = extractOpts || {};
|
|
645
|
+
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
646
|
+
throw new ArchiveReadError("archive-read/no-destination",
|
|
647
|
+
"extract: opts.destination must be a non-empty string (target directory)");
|
|
593
648
|
}
|
|
649
|
+
var destination = nodePath.resolve(extractOpts.destination);
|
|
650
|
+
if (!nodeFs.existsSync(destination)) {
|
|
651
|
+
nodeFs.mkdirSync(destination, { recursive: true });
|
|
652
|
+
}
|
|
653
|
+
var loaded = await _loadCD();
|
|
654
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
655
|
+
// Compose b.guardArchive on the metadata pass — operators with a
|
|
656
|
+
// posture set declared via opts.guardProfile get the cascade.
|
|
657
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extract.refused");
|
|
594
658
|
var written = [];
|
|
595
659
|
var bytesExtracted = 0;
|
|
596
660
|
var totalDecompressed = 0;
|
|
597
661
|
try {
|
|
598
662
|
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
663
|
+
_throwIfAborted();
|
|
599
664
|
var entry = loaded.entries[i];
|
|
600
665
|
// Skip directory + dangerous-by-default entry types unless the
|
|
601
666
|
// entry-type policy opts in.
|
|
@@ -693,10 +758,11 @@ function zip(adapter, opts) {
|
|
|
693
758
|
}
|
|
694
759
|
|
|
695
760
|
return {
|
|
696
|
-
kind:
|
|
697
|
-
inspect:
|
|
698
|
-
entries:
|
|
699
|
-
extract:
|
|
761
|
+
kind: "zip-random-access",
|
|
762
|
+
inspect: inspect,
|
|
763
|
+
entries: entries,
|
|
764
|
+
extract: extract,
|
|
765
|
+
extractEntries: extractEntries,
|
|
700
766
|
};
|
|
701
767
|
}
|
|
702
768
|
|
|
@@ -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
|
|
|
@@ -56,11 +56,10 @@
|
|
|
56
56
|
* - CVE-2024-12905 / CVE-2025-48387 (tar-fs path traversal).
|
|
57
57
|
* - CVE-2025-4138 / 4330 (Python tarfile data filter bypass).
|
|
58
58
|
*
|
|
59
|
-
* Out of scope
|
|
59
|
+
* Compression is via `b.archive.gz` composition (tar.gz). Out of scope
|
|
60
|
+
* (v1):
|
|
60
61
|
* - Sparse-file emission (read reconstructs them; write doesn't
|
|
61
62
|
* produce sparse).
|
|
62
|
-
* - Compression (tar.gz lands v0.12.9 via `b.archive.gz`
|
|
63
|
-
* composition).
|
|
64
63
|
* - BSD-tar extensions beyond pax.
|
|
65
64
|
*
|
|
66
65
|
* @card
|
|
@@ -155,6 +155,18 @@ function fromAssertion(opts) {
|
|
|
155
155
|
"fal.fromAssertion: channel must be 'front' or 'back'");
|
|
156
156
|
}
|
|
157
157
|
var hokBinding = opts.hokBinding;
|
|
158
|
+
// `bearerOnly: true` is the explicit alias for "no proof-of-possession
|
|
159
|
+
// binding" (hokBinding === null). It contradicts a non-null hokBinding;
|
|
160
|
+
// refuse the contradiction at the entry point rather than silently
|
|
161
|
+
// picking one — an operator who sets both has a config bug.
|
|
162
|
+
if (opts.bearerOnly === true) {
|
|
163
|
+
if (hokBinding !== undefined && hokBinding !== null) {
|
|
164
|
+
throw new AuthError("auth/bad-fal-opts",
|
|
165
|
+
"fal.fromAssertion: bearerOnly:true conflicts with hokBinding '" + hokBinding +
|
|
166
|
+
"' (bearerOnly forces no proof-of-possession binding)");
|
|
167
|
+
}
|
|
168
|
+
hokBinding = null;
|
|
169
|
+
}
|
|
158
170
|
if (hokBinding !== undefined && hokBinding !== null) {
|
|
159
171
|
if (hokBinding !== "mtls" && hokBinding !== "dpop" && hokBinding !== "saml-hok") {
|
|
160
172
|
throw new AuthError("auth/bad-fal-opts",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Defenses against the well-known JWT pitfalls:
|
|
30
30
|
*
|
|
31
|
-
* - alg confusion (CVE-2024-54150 / CVE-
|
|
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
|
|
@@ -175,8 +175,8 @@ function _assertAlgKtyMatch(alg, jwk) {
|
|
|
175
175
|
else if (alg === "ML-DSA-65" || alg === "ML-DSA-87") { expectedKty = "AKP"; }
|
|
176
176
|
else {
|
|
177
177
|
// Unknown alg — caller's alg allowlist should have rejected first;
|
|
178
|
-
// refuse here defensively (
|
|
179
|
-
// that skip downstream verification).
|
|
178
|
+
// refuse here defensively (CWE-347 alg-confusion class — unknown-alg
|
|
179
|
+
// paths that skip downstream verification; cf. CVE-2026-22817).
|
|
180
180
|
throw new AuthError("auth-jwt-external/unsupported-alg",
|
|
181
181
|
"_assertAlgKtyMatch: alg '" + alg + "' has no defined key-type binding");
|
|
182
182
|
}
|
|
@@ -336,7 +336,7 @@ async function verifyExternal(token, opts) {
|
|
|
336
336
|
|
|
337
337
|
// Decode header + payload.
|
|
338
338
|
var parts = token.split(".");
|
|
339
|
-
// CVE-2026-29000 / CVE-2026-
|
|
339
|
+
// CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 —
|
|
340
340
|
// JWE-bypass + alg-confusion. A 5-segment compact serialization is a
|
|
341
341
|
// JWE (RFC 7516); accepting it on a JWS verifier is the canonical
|
|
342
342
|
// confused-deputy shape. verifyExternal is JWS-only; refuse JWE
|
|
@@ -350,7 +350,7 @@ async function verifyExternal(token, opts) {
|
|
|
350
350
|
}); } catch (_e) { /* audit best-effort */ }
|
|
351
351
|
throw new AuthError("auth-jwt-external/jwe-refused",
|
|
352
352
|
"5-segment JWE token refused — verifyExternal only handles JWS " +
|
|
353
|
-
"(JWE bypass class — CVE-2026-29000 / CVE-2026-
|
|
353
|
+
"(JWE bypass class — CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950)");
|
|
354
354
|
}
|
|
355
355
|
if (parts.length !== 3) {
|
|
356
356
|
throw new AuthError("auth-jwt-external/malformed-jwt",
|
|
@@ -371,8 +371,9 @@ async function verifyExternal(token, opts) {
|
|
|
371
371
|
throw new AuthError("auth-jwt-external/unknown-crit",
|
|
372
372
|
"token declares 'crit' header — verifyExternal does not support critical extensions");
|
|
373
373
|
}
|
|
374
|
-
//
|
|
375
|
-
//
|
|
374
|
+
// Alg-allowlist gate (CWE-347 improper-sig-verification / CWE-757
|
|
375
|
+
// algorithm-downgrade) — refuse alg values outside the accepted list
|
|
376
|
+
// BEFORE any key lookup. The early refusal closes the class where an
|
|
376
377
|
// unknown / unsupported alg slips through to a downstream code path
|
|
377
378
|
// that interprets it permissively. The per-listed algorithm check
|
|
378
379
|
// above in the opts-validation loop refuses the OPERATOR'S allowlist
|
|
@@ -381,11 +382,11 @@ async function verifyExternal(token, opts) {
|
|
|
381
382
|
if (opts.algorithms.indexOf(header.alg) === -1) {
|
|
382
383
|
throw new AuthError("auth-jwt-external/alg-not-allowed",
|
|
383
384
|
"token alg='" + header.alg + "' not in allowed list [" + opts.algorithms.join(", ") +
|
|
384
|
-
"] (
|
|
385
|
+
"] (alg-allowlist gate — refused before key lookup)");
|
|
385
386
|
}
|
|
386
387
|
if (SUPPORTED_CLASSICAL_ALGS.indexOf(header.alg) === -1) {
|
|
387
388
|
throw new AuthError("auth-jwt-external/unsupported-alg",
|
|
388
|
-
"token alg='" + header.alg + "' is not in the verifier's supported set (
|
|
389
|
+
"token alg='" + header.alg + "' is not in the verifier's supported set (alg-allowlist gate)");
|
|
389
390
|
}
|
|
390
391
|
|
|
391
392
|
// Resolve key.
|
|
@@ -480,7 +481,10 @@ async function verifyExternal(token, opts) {
|
|
|
480
481
|
// weak iss validation. Constant-time compare defeats prefix-timing
|
|
481
482
|
// narrowing; emit a DISTINCT audit event (separate from sig-verify-
|
|
482
483
|
// fail) so detection signals lights up on the cross-realm shape
|
|
483
|
-
// independently of generic verification failures.
|
|
484
|
+
// independently of generic verification failures. The `typeof ... !==
|
|
485
|
+
// "string"` guard also rejects an array-valued iss (CVE-2025-30144,
|
|
486
|
+
// fast-jwt — an iss array `["attacker", "valid"]` passed an any-match
|
|
487
|
+
// check); only a single string iss is accepted.
|
|
484
488
|
if (typeof payload.iss !== "string" ||
|
|
485
489
|
!_issuerMatches(payload.iss, opts.issuer)) {
|
|
486
490
|
try { audit().safeEmit({
|
|
@@ -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;
|