@blamejs/core 0.7.21 → 0.7.23
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/csv.js +1 -1
- package/lib/guard-all.js +2 -2
- package/lib/guard-archive.js +2 -2
- package/lib/guard-csv.js +2 -2
- package/lib/guard-email.js +1 -1
- package/lib/guard-filename.js +1 -1
- package/lib/guard-html.js +1 -1
- package/lib/guard-json.js +1 -1
- package/lib/guard-markdown.js +1 -1
- package/lib/guard-svg.js +1 -1
- package/lib/guard-xml.js +1 -1
- package/lib/guard-yaml.js +1 -1
- package/lib/mail-dkim.js +53 -2
- package/lib/network-dns.js +75 -4
- package/package.json +1 -1
- package/sbom.cyclonedx.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.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.23** (2026-05-05) — DNS-over-HTTPS default-on. **Default-on DoH for outbound DNS** — `lib/network-dns.js` now uses Cloudflare DoH (`https://cloudflare-dns.com/dns-query`) for hostname resolution by default when neither `useDnsOverHttps()` nor `useDnsOverTls()` has been called explicitly. Privacy-respecting (encrypted DNS over TLS to a non-ISP resolver), aligned with Core Rule §3 ("security defaults are not opt-in"). **Local-form hosts route through node:dns** — RFC 6761 / 6762 special-form names (`localhost`, `*.localhost`, `*.local`, `*.test`, `*.invalid`, `*.internal`, `*.intranet`, `*.lan`, `*.home`, `*.corp`) AND IP literals skip DoH and use node:dns directly so `/etc/hosts` lookups + dev workflows continue to resolve correctly. **Operator opt-out via `b.network.dns.useSystemResolver()`** — split-horizon / internal-DNS deployments call this once at boot and every lookup routes through the OS resolver thereafter. **Env override `BLAMEJS_DNS_TRANSPORT`** — operators set `system` (force OS resolver), `dot` (force Cloudflare DNS-over-TLS at 1.1.1.1:853), or leave unset (default DoH). Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
|
+
|
|
13
|
+
- **0.7.22** (2026-05-05) — DKIM dual-signer + soc2-cc7 → soc2 posture rename. **`b.mail.dkim.dualSigner`** — RFC 8463 §3 transition signer that produces messages with BOTH a legacy RSA-SHA-256 DKIM-Signature AND an Ed25519-SHA-256 DKIM-Signature header. Receivers without Ed25519 support validate the RSA signature; receivers that prefer Ed25519 validate the post-quantum-friendlier signature. Operators rolling off RSA-SHA-256 wire `b.mail.dkim.dualSigner({ domain, rsa: { selector, privateKey }, eddsa: { selector, privateKey } })` and pass the result anywhere a regular DKIM signer is accepted; `sign()` produces a wire with two `DKIM-Signature` headers. Both signers are constructed eagerly at create-time (configuration errors surface at boot, not at first send). **soc2-cc7 → soc2 posture rename** — every guard's compliance posture name changes from `"soc2-cc7"` to `"soc2"`. The CC7-specific scoping was misleading (SOC 2 controls span CC1–CC9; the existing posture wasn't CC7-specific). Operators with `compliancePosture: "soc2-cc7"` MUST update to `compliancePosture: "soc2"` — the old name now throws `unknown compliance posture`. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
14
|
+
|
|
11
15
|
- **0.7.21** (2026-05-05) — small primitive batch (5 fixes): TLS 1.3 framework-wide minimum, `b.safeRedirect`, `b.pick`, audit-sign legacy compat-shim removed, webhook PQC signatures emit base64url. **Framework-wide TLS 1.3 minimum** — `index.js` sets `tls.DEFAULT_MIN_VERSION = "TLSv1.3"` once at boot, before any framework module loads `node:tls`. Applies to every TLS socket (outbound `https.request` / mail SMTP+STARTTLS / Redis-Postgres-Mongo TLS / `b.httpClient`, AND inbound `https.createServer` when blamejs is the listener). Per-call override still works for legacy peers. **`b.safeRedirect`** — open-redirect (CWE-601) defense. `b.safeRedirect.resolve(rawTarget, { allowedOrigins, allowedHosts, fallback })` returns the safe URL (or fallback). Refuses protocol-relative (`//attacker.com`), backslash variants (`\\\\attacker.com`), control-char-laden (CRLF header injection), and `data:` / `javascript:` schemes; same-origin paths (`/dashboard`) and fragments (`#x`) pass through; full URLs require explicit allowlist. Operator drops the result straight into `res.writeHead(302, { Location: ... })`. **`b.pick`** — mass-assignment (CWE-915 / OWASP API3:2023) defense. `b.pick(req.body, ["a", "b", ["nested", ["sub1"]]])` returns a NEW object with only allowlisted keys; prototype-pollution keys (`__proto__` / `constructor` / `prototype`) ALWAYS stripped even if listed. `opts.onUnknown: "throw"` rejects unknown keys instead of silently dropping. Nested allowlist syntax for object-shaped fields. **Audit-sign legacy compat-shim removed** — `lib/audit-sign.js` no longer falls back to `ml-dsa-87` for key files missing the `algorithm` field. Throws `KEY_FILE_MISSING_ALG` / `UNWRAPPED_MISSING_ALG` at load time; operators with legacy files rotate the key (deletes + regenerates) or hand-edit to add `"algorithm": "slh-dsa-shake-256f"`. Pre-v1 compat-shim sweep per the no-pre-v1-compat rule. **Webhook PQC signatures emit base64url** — `b.webhook` now signs to base64url (was hex). SLH-DSA-SHAKE-256f signatures are ~29.5 KB binary → ~40 KB base64url vs ~59 KB hex; the hex form blew past nginx default 8 KB / Cloudflare default 16 KB / many CDN edge limits. Verification accepts EITHER encoding for a transition window — base64url-shaped sig values decode as base64url; hex-shaped values decode as hex. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
16
|
|
|
13
17
|
- **0.7.20** (2026-05-05) — browser-hardening batch (5 fixes): CSRF Origin/Referer cross-check, default CSP gains Trusted Types, expanded Permissions-Policy, `__Host-` / `__Secure-` cookie prefix invariants, `b.middleware.fetchMetadata`. **CSRF Origin/Referer cross-check** (`b.middleware.csrfProtect`) — second-line defense alongside the double-submit token. State-changing requests whose Origin (or Referer when Origin is absent) doesn't resolve to the request's own origin are refused before the token check. Defaults ON; operator opt-out via `checkOrigin: false`; operator allowlist via `allowedOrigins: ["https://app.example.com"]`. Missing-Origin-AND-Missing-Referer (curl, server-to-server) defers to the token check. **Trusted Types in default CSP** (`b.middleware.securityHeaders`) — default CSP now includes `require-trusted-types-for 'script'; trusted-types 'allow-duplicates' default;`. Compatible browsers (Chrome 83+, Edge 83+) enforce typed-value DOM-sink writes (defends every untrusted-string-to-DOM XSS vector at runtime); Firefox + Safari ignore (no regression). Operators opt out by passing an explicit `csp:` value. **Expanded Permissions-Policy** — defaults now disable `browsing-topics=()`, `attribution-reporting=()`, `unload=()`, `interest-cohort=()`, `join-ad-interest-group=()`, `run-ad-auction=()`, `private-state-token-issuance=()`, `private-state-token-redemption=()`, `compute-pressure=()`, `hid=()`, `serial=()`, `idle-detection=()` — closes advertising / tracking / load-event-leak / device-API surfaces. Existing operator overrides via `permissionsPolicy: "..."` continue unchanged. **`__Host-` / `__Secure-` cookie prefix invariants** (`b.cookies.serialize`) — RFC 6265bis §4.1.3 enforced at serialize time. `__Secure-*` requires `secure: true`; `__Host-*` requires `secure: true` AND `path: "/"` AND no `domain:`. Each violation throws a typed `CookieError` with operator-actionable code (`cookies/prefix-secure-required`, `cookies/prefix-host-secure-required`, `cookies/prefix-host-path-required`, `cookies/prefix-host-no-domain`) instead of producing a malformed cookie that browsers silently reject. **`b.middleware.fetchMetadata`** — new fetch-metadata isolation primitive. Reads `Sec-Fetch-Site` / `Sec-Fetch-Mode` / `Sec-Fetch-Dest` and refuses cross-site state-changing requests by default. Operators opt in to specific destinations (e.g. `allowedDest: ["empty", "document"]`) or specific origins (`allowCrossSite: true`). Direct navigations (typed URL / bookmark — `Sec-Fetch-Site: none`) pass through; `same-origin` always passes; `same-site` configurable. Missing fetch-metadata (legacy browsers, server-to-server) deferred to other auth/CSRF layers per `allowMissing: true` default. Smoke 8481 / wiki e2e 178 / Linux container smoke 8481 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
46
46
|
- **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto + cryptographic erasure (`b.cryptoField.eraseRow`), signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA that issues clientAuth / serverAuth / dual-EKU certs with SAN entries and auto-detects the highest-PQC signature algorithm the vendored x509 library accepts (today: ECDSA-P384-SHA384 bridge; self-upgrades to SLH-DSA / ML-DSA when the X.509 ecosystem catches up), PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
|
|
47
47
|
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`, in-process CIDR fence via `b.middleware.networkAllowlist`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate (cloud-metadata IPs hard-denied unconditionally; private / loopback / link-local overridable per call), scheme + userinfo + per-host (wildcard / per-method) destination allowlist, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`); operator-tunable network configurability — env-driven NTP / NTS (RFC 8915 authenticated time), IPv4-or-IPv6 NTP servers, DNS with IPv6 / DoH / DoT (private-CA trust pinning via `opts.ca`) / cache / lookup timeout, outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`), runtime DPI trust-store CA additions, application-level heartbeats, TCP socket defaults (`b.network`).
|
|
48
48
|
- **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env), `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.).
|
|
49
|
-
- **Content-safety gates** — `b.gateContract` uniform composition contract (mode posture / hooks / forensic snapshot / decision cache / runtime cap). Family members: `b.guardCsv` (formula injection ASCII + full-width prefixes, dangerous-function denylist, bidi / homoglyph / control / null / BOM / zero-width detection, dialect ambiguity, CSV-bombs, numeric precision, schema-bound serializer); `b.guardHtml` (XSS / mXSS / DOM-clobbering / dangerous-tag / event-handler-family / dangerous-URL-scheme with entity-decode / CSS-injection in style attribute / IE conditional comments + token-level sanitize + always-correct `escapeText` / `escapeAttr` entity encoders); `b.guardSvg` (script / foreignObject / animation-element href hijack / DOCTYPE billion-laughs / XXE / SVGZ / cross-origin `<use>` SSRF / event-handlers + token-level sanitize). `b.guardArchive` (zip-slip / symlink + hardlink escape / decompression-ratio bombs (per-entry + aggregate) / nested-archive depth / duplicate-entry / case-insensitive collision / encryption-claim mismatch / format-claim mismatch via magic-byte detection — composes `b.guardFilename` for per-entry-name validation); `b.guardJson` (source-level prototype-pollution detection, duplicate-key, NaN/Infinity, JSON5 syntax, BOM, bidi, numeric-precision-loss, top-level-key allowlist, depth/breadth/array/string/node-count caps); `b.guardYaml` (deserialization-tag RCE via language-specific tag prefixes, billion-laughs alias recursion, Norway-problem implicit booleans, leading-zero octals, multi-document streams, duplicate keys, merge-key chains, depth+anchor+node caps); `b.guardXml` (XXE / billion-laughs / external-entity / parameter-entity / XInclude / xsi:schemaLocation / processing-instruction / CDATA / XML-signature-wrapping detection — DOCTYPE refused at all profile levels); `b.guardMarkdown` (source-level scan run BEFORE any markdown renderer sees the input — dangerous URL schemes in inline links + images + autolinks + reference-link definitions with HTML-entity decode bypass; whitespace-tolerant dangerous-tag matching per CVE-2026-30838; front-matter; HTML comments; code-fence language injection; catastrophic emphasis-run ReDoS per CVE-2025-6493 class; inline DOCTYPE; depth + link + image + autolink + ref-def caps); `b.guardEmail` (single-address + full RFC 822/5322 message validation — SMTP smuggling per CVE-2023-51764 / 51765 / 51766 / CVE-2026-32178 class via bare-CR + bare-LF + smuggled SMTP verbs; CRLF header injection; IDN homograph mixed-script domains with operator-opt-in `allowedScripts`; Punycode flag; display-name spoofing; IP-literal addresses; RFC 5322 comment syntax; multi-@; RFC 5321 length caps + RFC 5322 line cap; BOM injection). Filename safety: `b.guardFilename` (path traversal raw + percent-encoded + overlong-UTF-8 + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi spoofing + shell-exec / double-extension detection — standalone, wires into `b.fileUpload` via `filenameSafety`). All members ship strict / balanced / permissive profiles plus hipaa / pci-dss / gdpr / soc2
|
|
49
|
+
- **Content-safety gates** — `b.gateContract` uniform composition contract (mode posture / hooks / forensic snapshot / decision cache / runtime cap). Family members: `b.guardCsv` (formula injection ASCII + full-width prefixes, dangerous-function denylist, bidi / homoglyph / control / null / BOM / zero-width detection, dialect ambiguity, CSV-bombs, numeric precision, schema-bound serializer); `b.guardHtml` (XSS / mXSS / DOM-clobbering / dangerous-tag / event-handler-family / dangerous-URL-scheme with entity-decode / CSS-injection in style attribute / IE conditional comments + token-level sanitize + always-correct `escapeText` / `escapeAttr` entity encoders); `b.guardSvg` (script / foreignObject / animation-element href hijack / DOCTYPE billion-laughs / XXE / SVGZ / cross-origin `<use>` SSRF / event-handlers + token-level sanitize). `b.guardArchive` (zip-slip / symlink + hardlink escape / decompression-ratio bombs (per-entry + aggregate) / nested-archive depth / duplicate-entry / case-insensitive collision / encryption-claim mismatch / format-claim mismatch via magic-byte detection — composes `b.guardFilename` for per-entry-name validation); `b.guardJson` (source-level prototype-pollution detection, duplicate-key, NaN/Infinity, JSON5 syntax, BOM, bidi, numeric-precision-loss, top-level-key allowlist, depth/breadth/array/string/node-count caps); `b.guardYaml` (deserialization-tag RCE via language-specific tag prefixes, billion-laughs alias recursion, Norway-problem implicit booleans, leading-zero octals, multi-document streams, duplicate keys, merge-key chains, depth+anchor+node caps); `b.guardXml` (XXE / billion-laughs / external-entity / parameter-entity / XInclude / xsi:schemaLocation / processing-instruction / CDATA / XML-signature-wrapping detection — DOCTYPE refused at all profile levels); `b.guardMarkdown` (source-level scan run BEFORE any markdown renderer sees the input — dangerous URL schemes in inline links + images + autolinks + reference-link definitions with HTML-entity decode bypass; whitespace-tolerant dangerous-tag matching per CVE-2026-30838; front-matter; HTML comments; code-fence language injection; catastrophic emphasis-run ReDoS per CVE-2025-6493 class; inline DOCTYPE; depth + link + image + autolink + ref-def caps); `b.guardEmail` (single-address + full RFC 822/5322 message validation — SMTP smuggling per CVE-2023-51764 / 51765 / 51766 / CVE-2026-32178 class via bare-CR + bare-LF + smuggled SMTP verbs; CRLF header injection; IDN homograph mixed-script domains with operator-opt-in `allowedScripts`; Punycode flag; display-name spoofing; IP-literal addresses; RFC 5322 comment syntax; multi-@; RFC 5321 length caps + RFC 5322 line cap; BOM injection). Filename safety: `b.guardFilename` (path traversal raw + percent-encoded + overlong-UTF-8 + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi spoofing + shell-exec / double-extension detection — standalone, wires into `b.fileUpload` via `filenameSafety`). All members ship strict / balanced / permissive profiles plus hipaa / pci-dss / gdpr / soc2 compliance postures. `b.guardAll` is the registry + aggregator: every shipped guard ON by default; opt-out per guard with audited reason via `exceptFor: { name: { reason } }`. **As of v0.7.12, `b.fileUpload` and `b.staticServe` wire `b.guardAll.byExtension({ profile: "strict" })` automatically + `b.fileUpload` also wires `b.guardFilename.gate({ profile: "strict" })` as `filenameSafety`** — defense-in-depth applied without any explicit operator wiring. Operators opt out per host-primitive via `contentSafety: null` / `filenameSafety: null` (audited at create() with operator-supplied reason).
|
|
50
50
|
- **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); generic distributed pub/sub with cluster-table / Redis PUB/SUB / custom backends (`b.pubsub`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`); chunked file uploads with per-chunk SHA3-512 verification + atomic finalize + tombstone cleanup (`b.fileUpload`).
|
|
51
51
|
- **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks (local file rotation, generic webhook, OTLP/HTTP-JSON OR OTLP/gRPC to an OTel collector, AWS CloudWatch Logs via SigV4 with optional autoCreate, RFC 5424 syslog over UDP/TCP/TLS), OTLP/HTTP-JSON exporter for traces + metrics (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`); operator-callable boot-time security policy assertions (`b.security.assertProduction`) and tamper-evident config-baseline drift detection signed with the audit-signing key (`b.configDrift`).
|
|
52
52
|
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`).
|
package/lib/csv.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `b.guardCsv` — its `serialize` / `validate` / `sanitize` / `gate`
|
|
40
40
|
* surface handles every documented threat with a single profile choice
|
|
41
41
|
* (strict / balanced / permissive / email-attachment) or compliance
|
|
42
|
-
* posture (hipaa / pci-dss / gdpr / soc2
|
|
42
|
+
* posture (hipaa / pci-dss / gdpr / soc2).
|
|
43
43
|
*
|
|
44
44
|
* Throws CsvError (FrameworkError, permanent) on shape violations.
|
|
45
45
|
*/
|
package/lib/guard-all.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* - PROFILES — object map; must include the SHARED_PROFILES
|
|
44
44
|
* vocabulary (strict / balanced / permissive)
|
|
45
45
|
* - COMPLIANCE_POSTURES — object map; must include the SHARED_POSTURES
|
|
46
|
-
* vocabulary (hipaa / pci-dss / gdpr / soc2
|
|
46
|
+
* vocabulary (hipaa / pci-dss / gdpr / soc2)
|
|
47
47
|
* - gate(opts) — returns a b.gateContract-shaped gate
|
|
48
48
|
*
|
|
49
49
|
* The parity check at module load throws GuardAllError if a registered
|
|
@@ -95,7 +95,7 @@ var STANDALONE_GUARDS = [
|
|
|
95
95
|
// support. Adding a new shared profile / posture is a coordinated
|
|
96
96
|
// cross-guard change — every member must implement it.
|
|
97
97
|
var SHARED_PROFILES = Object.freeze(["strict", "balanced", "permissive"]);
|
|
98
|
-
var SHARED_POSTURES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2
|
|
98
|
+
var SHARED_POSTURES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
|
|
99
99
|
|
|
100
100
|
// ---- Registry parity check (runs at module load) ----
|
|
101
101
|
|
package/lib/guard-archive.js
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
* permissive — symlinks + hardlinks within root allowed; nested-depth
|
|
118
118
|
* 4; 100000 entries; 10 GiB total; 1000:1 ratio.
|
|
119
119
|
*
|
|
120
|
-
* Compliance postures: hipaa / pci-dss / gdpr / soc2
|
|
120
|
+
* Compliance postures: hipaa / pci-dss / gdpr / soc2 — strict
|
|
121
121
|
* overlay + forensic snapshots.
|
|
122
122
|
*/
|
|
123
123
|
|
|
@@ -248,7 +248,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
248
248
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
249
249
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
250
250
|
}),
|
|
251
|
-
"soc2
|
|
251
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
252
252
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
253
253
|
}),
|
|
254
254
|
});
|
package/lib/guard-csv.js
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* validation.
|
|
48
48
|
* - Profiles: strict (OWASP-aligned, prefix-tab default per OWASP) /
|
|
49
49
|
* balanced / permissive / email-attachment.
|
|
50
|
-
* - Compliance postures: hipaa / pci-dss / gdpr / soc2
|
|
50
|
+
* - Compliance postures: hipaa / pci-dss / gdpr / soc2.
|
|
51
51
|
* - Operator extensibility: profile composition, custom rules, hooks
|
|
52
52
|
* (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse /
|
|
53
53
|
* onAudit), threat-intel feeds, sandbox isolation, snapshot tests.
|
|
@@ -275,7 +275,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
275
275
|
piiPolicy: "redact",
|
|
276
276
|
forensicSnippetBytes: FORENSIC_SNIPPET_GDPR,
|
|
277
277
|
},
|
|
278
|
-
"soc2
|
|
278
|
+
"soc2": {
|
|
279
279
|
formulaInjectionPolicy: "prefix-tab",
|
|
280
280
|
bidiCharPolicy: "reject",
|
|
281
281
|
controlCharPolicy: "reject",
|
package/lib/guard-email.js
CHANGED
|
@@ -265,7 +265,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
265
265
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
266
266
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
267
267
|
}),
|
|
268
|
-
"soc2
|
|
268
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
269
269
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
270
270
|
}),
|
|
271
271
|
});
|
package/lib/guard-filename.js
CHANGED
|
@@ -234,7 +234,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
234
234
|
leadingTrailingPolicy: "strip",
|
|
235
235
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
236
236
|
},
|
|
237
|
-
"soc2
|
|
237
|
+
"soc2": {
|
|
238
238
|
bidiPolicy: "reject", controlPolicy: "reject", nullBytePolicy: "reject",
|
|
239
239
|
zeroWidthPolicy: "reject", traversalPolicy: "reject",
|
|
240
240
|
reservedCharPolicy: "reject", reservedNamePolicy: "reject",
|
package/lib/guard-html.js
CHANGED
package/lib/guard-json.js
CHANGED
|
@@ -204,7 +204,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
204
204
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
205
205
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
206
206
|
}),
|
|
207
|
-
"soc2
|
|
207
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
208
208
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
209
209
|
}),
|
|
210
210
|
});
|
package/lib/guard-markdown.js
CHANGED
|
@@ -213,7 +213,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
213
213
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
214
214
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
215
215
|
}),
|
|
216
|
-
"soc2
|
|
216
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
217
217
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
218
218
|
}),
|
|
219
219
|
});
|
package/lib/guard-svg.js
CHANGED
package/lib/guard-xml.js
CHANGED
|
@@ -143,7 +143,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
143
143
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
144
144
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
145
145
|
}),
|
|
146
|
-
"soc2
|
|
146
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
147
147
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
148
148
|
}),
|
|
149
149
|
});
|
package/lib/guard-yaml.js
CHANGED
|
@@ -183,7 +183,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
|
|
|
183
183
|
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
184
184
|
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
185
185
|
}),
|
|
186
|
-
"soc2
|
|
186
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
187
187
|
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
188
188
|
}),
|
|
189
189
|
});
|
package/lib/mail-dkim.js
CHANGED
|
@@ -358,11 +358,62 @@ function create(opts) {
|
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
// dualSigner — RFC 8463 §3 transition signer. Produces messages with
|
|
362
|
+
// BOTH a legacy RSA-SHA-256 DKIM-Signature AND an Ed25519-SHA-256
|
|
363
|
+
// DKIM-Signature header. Receivers that don't yet support Ed25519
|
|
364
|
+
// validate the RSA signature; receivers that prefer Ed25519 validate
|
|
365
|
+
// the post-quantum-friendlier signature. The transition pattern is
|
|
366
|
+
// the recommended path for moving the operator's domain off RSA-SHA-
|
|
367
|
+
// 256 without breaking older verifiers.
|
|
368
|
+
//
|
|
369
|
+
// var dual = b.mail.dkim.dualSigner({
|
|
370
|
+
// domain: "example.com",
|
|
371
|
+
// rsa: { selector: "rsa1", privateKey: rsaPemKey },
|
|
372
|
+
// eddsa: { selector: "eddsa1", privateKey: ed25519PemKey },
|
|
373
|
+
// // every other create() opt is shared (canonicalization,
|
|
374
|
+
// // headersToSign, audit) but can be overridden per algorithm.
|
|
375
|
+
// });
|
|
376
|
+
// var signed = dual.sign(rfc822Wire);
|
|
377
|
+
// // → wire with two DKIM-Signature: headers (RSA first, Ed25519 second)
|
|
378
|
+
//
|
|
379
|
+
// Both signers are constructed eagerly at create-time (configuration
|
|
380
|
+
// errors surface at boot, not at first send). The combined sign()
|
|
381
|
+
// applies the RSA signer first, then the Ed25519 signer on top.
|
|
382
|
+
function dualSigner(opts) {
|
|
383
|
+
if (!opts || !opts.rsa || !opts.eddsa) {
|
|
384
|
+
throw new DkimError("dkim/dual-signer-missing",
|
|
385
|
+
"dualSigner requires both opts.rsa and opts.eddsa");
|
|
386
|
+
}
|
|
387
|
+
if (!opts.domain) {
|
|
388
|
+
throw new DkimError("dkim/dual-signer-missing-domain",
|
|
389
|
+
"dualSigner requires opts.domain");
|
|
390
|
+
}
|
|
391
|
+
function _merge(base, alg, override) {
|
|
392
|
+
return Object.assign({}, base, { algorithm: alg }, override);
|
|
393
|
+
}
|
|
394
|
+
var sharedBase = {};
|
|
395
|
+
var commonKeys = ["domain", "headersToSign", "canonicalization", "audit"];
|
|
396
|
+
for (var i = 0; i < commonKeys.length; i += 1) {
|
|
397
|
+
if (opts[commonKeys[i]] !== undefined) sharedBase[commonKeys[i]] = opts[commonKeys[i]];
|
|
398
|
+
}
|
|
399
|
+
var rsaSigner = create(_merge(sharedBase, "rsa-sha256", opts.rsa));
|
|
400
|
+
var eddsaSigner = create(_merge(sharedBase, "ed25519-sha256", opts.eddsa));
|
|
401
|
+
return {
|
|
402
|
+
sign: function (rfc822) {
|
|
403
|
+
var afterRsa = rsaSigner.sign(rfc822);
|
|
404
|
+
return eddsaSigner.sign(afterRsa);
|
|
405
|
+
},
|
|
406
|
+
rsa: rsaSigner,
|
|
407
|
+
eddsa: eddsaSigner,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
361
411
|
// Test-only exports for unit testing the canonicalization primitives
|
|
362
412
|
// directly without going through a full sign() round.
|
|
363
413
|
module.exports = {
|
|
364
|
-
create:
|
|
365
|
-
|
|
414
|
+
create: create,
|
|
415
|
+
dualSigner: dualSigner,
|
|
416
|
+
DkimError: DkimError,
|
|
366
417
|
_canonHeaderRelaxedForTest: _canonHeaderRelaxed,
|
|
367
418
|
_canonBodyRelaxedForTest: _canonBodyRelaxed,
|
|
368
419
|
_canonBodySimpleForTest: _canonBodySimple,
|
package/lib/network-dns.js
CHANGED
|
@@ -24,6 +24,7 @@ var IPV6_HEX_GROUPS = C.BYTES.bytes(8); // IPv6 16-bit hex groups
|
|
|
24
24
|
var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
|
|
25
25
|
|
|
26
26
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
27
|
+
var safeEnv = require("./parsers/safe-env");
|
|
27
28
|
|
|
28
29
|
var STATE = {
|
|
29
30
|
servers: null,
|
|
@@ -34,8 +35,51 @@ var STATE = {
|
|
|
34
35
|
cacheNegativeTtlMs: 0,
|
|
35
36
|
doh: null,
|
|
36
37
|
dot: null,
|
|
38
|
+
// Default-on secure DNS (DoH via Cloudflare) when neither doh nor dot
|
|
39
|
+
// is operator-configured AND no opt-out env var is set. Operators
|
|
40
|
+
// who explicitly want the system resolver call useSystemResolver()
|
|
41
|
+
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on per Core Rule §3
|
|
42
|
+
// ("security defaults are not opt-in").
|
|
43
|
+
systemResolver: false,
|
|
37
44
|
};
|
|
38
45
|
|
|
46
|
+
// Default DoH provider when nothing is configured. Cloudflare's
|
|
47
|
+
// 1.1.1.1 service is privacy-respecting, free, and global.
|
|
48
|
+
var DEFAULT_DOH_URL = "https://cloudflare-dns.com/dns-query";
|
|
49
|
+
|
|
50
|
+
// RFC 6761 + RFC 6762 + operator-internal special-form host names that
|
|
51
|
+
// public DoH/DoT providers don't resolve. Local lookups for these names
|
|
52
|
+
// route through node:dns which honours /etc/hosts + LDH locally.
|
|
53
|
+
var LOCAL_SUFFIXES = [".localhost", ".local", ".test", ".invalid",
|
|
54
|
+
".internal", ".intranet", ".lan", ".home", ".corp"];
|
|
55
|
+
function _isLocalFormHost(host) {
|
|
56
|
+
if (typeof host !== "string" || host.length === 0) return true;
|
|
57
|
+
if (host === "localhost") return true;
|
|
58
|
+
// IP literal — skip DNS resolution entirely (caller passes through).
|
|
59
|
+
if (net.isIP(host)) return true;
|
|
60
|
+
var lc = host.toLowerCase();
|
|
61
|
+
for (var i = 0; i < LOCAL_SUFFIXES.length; i += 1) {
|
|
62
|
+
if (lc.length > LOCAL_SUFFIXES[i].length &&
|
|
63
|
+
lc.slice(-LOCAL_SUFFIXES[i].length) === LOCAL_SUFFIXES[i]) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _ensureSecureDefault() {
|
|
71
|
+
if (STATE.doh || STATE.dot || STATE.systemResolver) return;
|
|
72
|
+
var override = safeEnv.readVar("BLAMEJS_DNS_TRANSPORT");
|
|
73
|
+
if (override === "system") { STATE.systemResolver = true; return; }
|
|
74
|
+
if (override === "dot") {
|
|
75
|
+
// Cloudflare 1.1.1.1 over TLS, port 853.
|
|
76
|
+
STATE.dot = { host: "1.1.1.1", port: 853, servername: "1.1.1.1", ca: null }; // allow:raw-byte-literal — IANA-assigned DoT port
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Default: DoH via Cloudflare.
|
|
80
|
+
STATE.doh = { url: DEFAULT_DOH_URL, method: null, ca: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
var POSITIVE_CACHE = new Map();
|
|
40
84
|
var NEGATIVE_CACHE = new Map();
|
|
41
85
|
|
|
@@ -144,6 +188,20 @@ function setCacheTtlMs(ms, negativeMs) {
|
|
|
144
188
|
if (ms === 0) _clearCache();
|
|
145
189
|
}
|
|
146
190
|
|
|
191
|
+
// Operator opt-out from the default-on secure DNS. After calling this,
|
|
192
|
+
// every lookup routes through node:dns (the OS resolver). Equivalent
|
|
193
|
+
// to setting BLAMEJS_DNS_TRANSPORT=system. Operators on internal-DNS
|
|
194
|
+
// deployments (split-horizon, custom resolver appliances) need this
|
|
195
|
+
// to keep their hostnames resolving locally.
|
|
196
|
+
function useSystemResolver() {
|
|
197
|
+
STATE.doh = null;
|
|
198
|
+
STATE.dot = null;
|
|
199
|
+
STATE.systemResolver = true;
|
|
200
|
+
_resetDotPool();
|
|
201
|
+
_clearCache();
|
|
202
|
+
_emitObs("network.dns.system_resolver.set", {});
|
|
203
|
+
}
|
|
204
|
+
|
|
147
205
|
function useDnsOverHttps(opts) {
|
|
148
206
|
opts = opts || {};
|
|
149
207
|
validateOpts(opts, ["provider", "url", "method", "ca"], "dns.useDnsOverHttps");
|
|
@@ -514,15 +572,27 @@ async function lookup(host, opts) {
|
|
|
514
572
|
}
|
|
515
573
|
_emitObs("network.dns.lookup.requested", { family: cacheKey });
|
|
516
574
|
var startMs = _now();
|
|
575
|
+
// Resolve secure-DNS default on first use. Idempotent.
|
|
576
|
+
_ensureSecureDefault();
|
|
577
|
+
|
|
578
|
+
// Special-form hostnames per RFC 6761 (localhost / .test / .invalid /
|
|
579
|
+
// .localhost) and operator-internal suffixes never hit DoH/DoT —
|
|
580
|
+
// public DoH providers don't resolve these. Route through node:dns
|
|
581
|
+
// (which checks /etc/hosts and the OS resolver). This keeps tests +
|
|
582
|
+
// dev workflows working while public-internet resolution still goes
|
|
583
|
+
// through the secure transport.
|
|
584
|
+
var isLocalForm = _isLocalFormHost(host);
|
|
585
|
+
|
|
517
586
|
try {
|
|
518
587
|
var addrs;
|
|
519
|
-
if (STATE.doh) {
|
|
588
|
+
if (STATE.doh && !isLocalForm) {
|
|
520
589
|
addrs = await _dualStack(_dohLookup, host, family);
|
|
521
|
-
} else if (STATE.dot) {
|
|
590
|
+
} else if (STATE.dot && !isLocalForm) {
|
|
522
591
|
addrs = await _dualStack(_dotLookup, host, family);
|
|
523
592
|
} else {
|
|
593
|
+
// System resolver (operator explicit opt-out via useSystemResolver).
|
|
524
594
|
var nodeOpts = { all: true };
|
|
525
|
-
if (family === 4 || family === 6) nodeOpts.family = family;
|
|
595
|
+
if (family === 4 || family === 6) nodeOpts.family = family; // allow:raw-byte-literal — IPv4/IPv6 family literals
|
|
526
596
|
addrs = await _withTimeout(dnsPromises.lookup(host, nodeOpts), STATE.lookupTimeoutMs, host);
|
|
527
597
|
if (!Array.isArray(addrs)) addrs = [addrs];
|
|
528
598
|
}
|
|
@@ -606,7 +676,7 @@ function _stateForTest() { return STATE; }
|
|
|
606
676
|
function _resetForTest() {
|
|
607
677
|
STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
|
|
608
678
|
STATE.lookupTimeoutMs = 0; STATE.cacheTtlMs = 0; STATE.cacheNegativeTtlMs = 0;
|
|
609
|
-
STATE.doh = null; STATE.dot = null;
|
|
679
|
+
STATE.doh = null; STATE.dot = null; STATE.systemResolver = false;
|
|
610
680
|
_clearCache();
|
|
611
681
|
_resetDotPool();
|
|
612
682
|
}
|
|
@@ -620,6 +690,7 @@ module.exports = {
|
|
|
620
690
|
setCacheTtlMs: setCacheTtlMs,
|
|
621
691
|
useDnsOverHttps: useDnsOverHttps,
|
|
622
692
|
useDnsOverTls: useDnsOverTls,
|
|
693
|
+
useSystemResolver: useSystemResolver,
|
|
623
694
|
lookup: lookup,
|
|
624
695
|
resolve4: resolve4,
|
|
625
696
|
resolve6: resolve6,
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:2ed35351-9ec7-4e19-9471-f35604ba988a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-05T06:
|
|
8
|
+
"timestamp": "2026-05-05T06:39:17.238Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.23",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.23",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.23",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|