@blamejs/core 0.10.2 → 0.10.3
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 +1 -0
- package/lib/crypto.js +68 -8
- package/lib/middleware/compose-pipeline.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.10.x
|
|
10
10
|
|
|
11
|
+
- v0.10.3 (2026-05-16) — **`b.crypto` hardening — three entry-tier refusals on hot paths.** **(a) `b.crypto.timingSafeEqual` rejects non-Buffer / non-string inputs** — previous `Buffer.from(String(x))` coercion let a prototype-pollution-influenced caller (an Object whose `toString` returns attacker-chosen bytes) redirect the compare through bytes unrelated to the supplied value. Now throws `TypeError` at the entry boundary; string args use explicit `Buffer.from(s, "utf8")` instead of bare coercion. **(b) `b.crypto.hashCertFingerprint` caps PEM input at 64 KiB** — the `/-----BEGIN .+? -----END/` lazy-quantifier on this hot path (mTLS bootstrap / webhook verification / peer-cert pinning) is polynomial-ReDoS-class on multi-MB attacker-controlled input ([CodeQL js/polynomial-redos](https://codeql.github.com/codeql-query-help/javascript/js-polynomial-redos/)). 64 KiB covers a P-384 cert + full chain at ~3× margin; larger inputs throw `TypeError` before the regex runs. **(c) `b.crypto.namespaceHash` refuses CR / LF in string-typed `value`** — closes a log-injection / record-separator surface where an attacker-controlled HTTP header (e.g. `Idempotency-Key`) could smuggle line-break bytes into any consumer that logs the value verbatim before hashing (debug paths, audit envelopes, derived-column shadow logs). NUL is NOT refused — multiple internal callers (`b.agent.idempotency` / `b.mail.greylist` / `b.middleware.composePipeline`) use NUL as a composite-key separator, and NUL is not a log-injection byte in any standard logger. `Buffer` / `Uint8Array` inputs remain operator-side opaque bytes by contract — `namespaceHash` digests them as raw bytes, not as text, so the control-char gate does not apply there either. **Operator impact:** any caller passing a number / Object / boolean to `b.crypto.timingSafeEqual` now throws at the entry boundary instead of silently comparing coerced bytes — the API contract was already documented as Buffer-or-string, this enforces it. PEM strings larger than 64 KiB to `b.crypto.hashCertFingerprint` now throw — operators with bespoke multi-cert bundles split the inputs before calling. `namespaceHash` callers passing strings with embedded CR / LF now throw — operators ingesting attacker-influenced text validate / strip line-break bytes at the boundary, or hash opaque bytes via `Buffer` / `Uint8Array`. References: [OWASP Log Injection](https://owasp.org/www-community/attacks/Log_Injection), [CWE-117](https://cwe.mitre.org/data/definitions/117.html), [CWE-1333 ReDoS](https://cwe.mitre.org/data/definitions/1333.html).
|
|
11
12
|
- v0.10.2 (2026-05-16) — **CVE backstops layered on top of v0.10.0.** Five additional refusals across `b.guardRegex`, `b.otelExport`, `b.guardXml`, `b.guardGraphql`, plus a host-side ingress route for `b.cli`. Every change is opt-out (refusal at every profile); no API removals. **(a) `b.guardRegex` glob-shape detectors with explicit `inputKind` gate** — new `consecutiveStarPolicy` + `nestedExtglobPolicy` (defaults `"reject"`) + `maxConsecutiveStars` (default 2) + `inputKind: "regex" | "glob"` (default `"regex"`). The glob-shape detectors fire ONLY when the caller passes `inputKind: "glob"` — ECMAScript regex syntax cannot produce `***` (SyntaxError) and the extglob heads `*(`/`+(`/`?(`/`@(`/`!(` collide with valid `quantifier + capturing group` shapes, so applying these detectors to regex inputs is false-positive territory. Callers handling glob fragments (picomatch / micromatch-style patterns) opt in via `inputKind: "glob"` and get refusals for ≥3 consecutive `*` metacharacters ([CVE-2026-26996](https://nvd.nist.gov/vuln/detail/CVE-2026-26996) — O(4^N) backtracking on non-matching literal) and for any extglob whose body contains another extglob ([CVE-2026-33671](https://nvd.nist.gov/vuln/detail/CVE-2026-33671) — picomatch nested-quantifier backtracking). `**` recursive-glob stays permitted under `maxConsecutiveStars: 2`. **(b) `b.cli --ignore` ReDoS ingress closure** — `cli --ignore <pattern>` arguments route through `b.guardRegex.sanitize({ profile: "strict" })` before reaching `new RegExp(pattern)`. Strict-profile refusal of nested-quantifier / lookaround-quantifier / unbounded-bounded-repeat shapes still applies in default `inputKind: "regex"` mode, closing the host-side surface for the classic ReDoS classes. **(c) `b.otelExport.flush()` response cap** — every outbound OTLP request now pins `maxResponseBytes: 1 MiB` + a typed `errorClass`, so a malicious / misconfigured collector cannot exhaust memory in the export loop ([CVE-2026-40891](https://nvd.nist.gov/vuln/detail/CVE-2026-40891) / [CVE-2026-40182](https://nvd.nist.gov/vuln/detail/CVE-2026-40182) class). **(d) `b.guardXml` numeric-character-reference fan-out cap** — new `maxNumericCharRefs` opt (strict 1024 / balanced 16384 / permissive 262144). NCRs are counted independently of `entityPolicy`, so a signed-XML path that legitimately permits entity expansion cannot accidentally disable the NCR cap ([CVE-2026-26278](https://nvd.nist.gov/vuln/detail/CVE-2026-26278) / [CVE-2026-33036](https://nvd.nist.gov/vuln/detail/CVE-2026-33036) — billion-NCR fan-out class). **(e) `b.guardGraphql` prototype-pollution refusal** — refuses `__proto__` / `constructor` / `prototype` as top-level variable keys (`Object.prototype.hasOwnProperty.call(variables, ...)` check, sidesteps a poisoned-prototype `in` lookup) AND as field / alias / `$variable` identifiers in the query body, including the no-whitespace alias form `query { a:__proto__ }` (the colon is a valid identifier-position prefix). Refused at every profile, severity `critical` ([CVE-2026-32621](https://nvd.nist.gov/vuln/detail/CVE-2026-32621) class). **(f) `b.auth.sdJwtVc.present()` defense-in-depth comment** — documents that the holder-side pre-parse of `_sd_alg` reads from unsigned bytes safely because `verify()` re-parses from the cryptographically-verified signing input; no behavioral change. **Regression coverage** — `test/fixtures/exploit-corpus/corpus.json` gains four entries: glob-mode positive refusal for `***+nonmatch` and `*(*(a))`, regex-mode pass for `a*(b+(c))` (false-positive class the design refused to ship), and the colon-prefix GraphQL alias `query { a:__proto__ }`. **Operator impact:** existing operators see no change in default behavior — the new glob detectors are opt-in via `inputKind: "glob"`. Operators wiring `b.guardRegex` over glob fragments (file-pattern allowlists, rsync-style rules) opt in and get the CVE-2026-26996 / -33671 refusals; opt back out per call via `consecutiveStarPolicy: "allow"` / `nestedExtglobPolicy: "allow"`. `b.guardXml` operators on signed-XML pipelines opt out via `maxNumericCharRefs: Infinity` if they bound NCRs upstream. GraphQL variable / query-body refusals are not opt-out — `__proto__` / `constructor` / `prototype` are never legitimate identifiers in operator-supplied input. References: [picomatch CVE-2024-4067 family](https://nvd.nist.gov/vuln/detail/CVE-2024-4067), [OWASP ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS), [OWASP XXE / Billion Laughs](https://owasp.org/www-community/vulnerabilities/XML_Entity_Expansion), [GraphQL Server Security Best Practices](https://www.apollographql.com/docs/router/configuration/overview/).
|
|
12
13
|
- v0.10.1 (2026-05-16) — **First npm-published v0.10.x artifact.** v0.10.0 was tagged + released on GitHub but its npm-publish workflow OOM'd at the lint+smoke gate (default Node ~4GB heap couldn't load the expanded mail-stack + audit-fix test surface). v0.10.1 adds `NODE_OPTIONS=--max-old-space-size=8192` to the workflow's smoke step so the parent process gets the same headroom the forked test workers already get. No runtime / API changes from v0.10.0 — every primitive, posture, and security default ships exactly as documented in the v0.10.0 release notes below. Operators who fetched the v0.10.0 git tag can re-tag from v0.10.1 (`git fetch origin v0.10.1`) to land on the npm-published commit; the framework code itself is byte-identical apart from the workflow file + version bump.
|
|
13
14
|
- v0.10.0 (2026-05-16) — **Mail-stack feature-complete + cross-surface hardening.** Bundled minor closing the blamepost mail-stack roadmap (five new operator-facing namespaces) plus a multi-domain hardening sweep across auth / crypto / vendor data / mail-protocol / mail-auth / agent substrate / Node.js CVE backstops.
|
package/lib/crypto.js
CHANGED
|
@@ -55,6 +55,11 @@ var C = require("./constants");
|
|
|
55
55
|
// require() inside setImmediate (top-of-file requires per rule §3).
|
|
56
56
|
var lazyRequire = require("./lazy-require");
|
|
57
57
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
58
|
+
// safe-buffer hosts the canonical hasCrlf(s) helper used by every
|
|
59
|
+
// log-injection / CRLF-smuggling refusal in the framework. Lazy-
|
|
60
|
+
// loaded because safe-buffer.js itself imports b.crypto for
|
|
61
|
+
// hex-compare helpers (circular).
|
|
62
|
+
var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
|
|
58
63
|
|
|
59
64
|
// Streaming-hash algorithm allowlist. Mirrors the framework's PQC-
|
|
60
65
|
// first crypto policy: SHA3 / SHAKE family is the default surface;
|
|
@@ -399,12 +404,15 @@ function generateKeyPair(algorithm, options) {
|
|
|
399
404
|
* @since 0.1.0
|
|
400
405
|
* @related b.crypto.hmacSha3
|
|
401
406
|
*
|
|
402
|
-
* Constant-time equality comparison.
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
407
|
+
* Constant-time equality comparison. Accepts only Buffer or string
|
|
408
|
+
* inputs — non-string non-Buffer arguments throw at the entry tier so
|
|
409
|
+
* a `Object.prototype.toString`-poisoned caller can't redirect the
|
|
410
|
+
* compare through arbitrary attacker-controlled bytes. Returns
|
|
411
|
+
* `false` immediately when lengths differ (length itself is not a
|
|
412
|
+
* secret), then routes equal-length inputs through
|
|
413
|
+
* `crypto.timingSafeEqual`. Use when comparing HMAC digests, session
|
|
414
|
+
* tokens, password-reset codes, or any attacker-influenced value
|
|
415
|
+
* where a timing oracle would leak bits.
|
|
408
416
|
*
|
|
409
417
|
* @example
|
|
410
418
|
* var expected = b.crypto.hmacSha3("server-key", "payload");
|
|
@@ -413,8 +421,25 @@ function generateKeyPair(algorithm, options) {
|
|
|
413
421
|
* // → true when bytes match, false otherwise (no early exit on mismatch)
|
|
414
422
|
*/
|
|
415
423
|
function timingSafeEqual(a, b) {
|
|
416
|
-
|
|
417
|
-
|
|
424
|
+
// Entry-tier validation. The prior `Buffer.from(String(x))` coercion
|
|
425
|
+
// let a prototype-pollution-influenced caller (Object whose toString
|
|
426
|
+
// returns attacker-chosen bytes) redirect the compare through bytes
|
|
427
|
+
// that have nothing to do with the supplied value. Refuse any
|
|
428
|
+
// non-string non-Buffer input outright.
|
|
429
|
+
if (!Buffer.isBuffer(a) && typeof a !== "string") {
|
|
430
|
+
throw new TypeError(
|
|
431
|
+
"crypto.timingSafeEqual: argument 'a' must be a Buffer or string, got " +
|
|
432
|
+
(a === null ? "null" : typeof a)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (!Buffer.isBuffer(b) && typeof b !== "string") {
|
|
436
|
+
throw new TypeError(
|
|
437
|
+
"crypto.timingSafeEqual: argument 'b' must be a Buffer or string, got " +
|
|
438
|
+
(b === null ? "null" : typeof b)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
var bufA = Buffer.isBuffer(a) ? a : Buffer.from(a, "utf8");
|
|
442
|
+
var bufB = Buffer.isBuffer(b) ? b : Buffer.from(b, "utf8");
|
|
418
443
|
if (bufA.length !== bufB.length) return false;
|
|
419
444
|
return nodeCrypto.timingSafeEqual(bufA, bufB);
|
|
420
445
|
}
|
|
@@ -581,8 +606,10 @@ function namespaceHash(prefix, value, opts) {
|
|
|
581
606
|
// caller surfaces the type error explicitly rather than silently
|
|
582
607
|
// hashing `[object Object]`.
|
|
583
608
|
var valueStr;
|
|
609
|
+
var valueWasString = false;
|
|
584
610
|
if (typeof value === "string") {
|
|
585
611
|
valueStr = value;
|
|
612
|
+
valueWasString = true;
|
|
586
613
|
} else if (Buffer.isBuffer(value)) {
|
|
587
614
|
valueStr = value.toString("utf8");
|
|
588
615
|
} else if (value instanceof Uint8Array) {
|
|
@@ -592,6 +619,25 @@ function namespaceHash(prefix, value, opts) {
|
|
|
592
619
|
"crypto.namespaceHash: value must be a string, Buffer, or Uint8Array"
|
|
593
620
|
);
|
|
594
621
|
}
|
|
622
|
+
// Refuse CR / LF in string-typed values. The prior gap let an
|
|
623
|
+
// attacker-controlled string `value` (e.g. an HTTP header that
|
|
624
|
+
// becomes an Idempotency-Key) smuggle log-injection / record-
|
|
625
|
+
// separator bytes into any consumer that logs the value verbatim
|
|
626
|
+
// before hashing (debug paths, audit envelopes, derived-column
|
|
627
|
+
// shadow logs). NUL is NOT refused — multiple internal callers
|
|
628
|
+
// use NUL as a composite-key separator (`method\0actorId\0key`
|
|
629
|
+
// shape in agent-idempotency / mail-greylist / compose-pipeline),
|
|
630
|
+
// and NUL is not a log-injection byte in any standard logger. NUL
|
|
631
|
+
// in operator-supplied content is the operator-boundary
|
|
632
|
+
// responsibility. Buffer / Uint8Array inputs remain operator-side
|
|
633
|
+
// opaque bytes by contract — namespaceHash treats them as raw
|
|
634
|
+
// bytes to be digested without rendering, so the control-char
|
|
635
|
+
// gate does not apply there either.
|
|
636
|
+
if (valueWasString && safeBuffer().hasCrlf(valueStr)) {
|
|
637
|
+
throw new TypeError(
|
|
638
|
+
"crypto.namespaceHash: value (string-typed) contains CR / LF — refuse"
|
|
639
|
+
);
|
|
640
|
+
}
|
|
595
641
|
return hash(prefix + ":" + valueStr, "sha3-512").toString("hex");
|
|
596
642
|
}
|
|
597
643
|
|
|
@@ -1664,6 +1710,20 @@ function _pemToDer(pemOrDer) {
|
|
|
1664
1710
|
if (typeof pemOrDer !== "string") {
|
|
1665
1711
|
throw new TypeError("crypto.hashCertFingerprint: input must be a Buffer (DER) or a PEM-encoded string");
|
|
1666
1712
|
}
|
|
1713
|
+
// Bound the regex input. The /-----BEGIN .+? -----END/ pattern is
|
|
1714
|
+
// lazy-quantified, which CodeQL flags as polynomial-ReDoS
|
|
1715
|
+
// (js/polynomial-redos) when fed multi-MB attacker-controlled input
|
|
1716
|
+
// — every backtrack step is O(n) and the pattern is on a hot path
|
|
1717
|
+
// for mTLS bootstrap / webhook verification / peer-cert pinning.
|
|
1718
|
+
// 64 KiB caps the largest plausible PEM (a P-384 cert + chain) at
|
|
1719
|
+
// ~3× margin while refusing pathological inputs outright.
|
|
1720
|
+
if (pemOrDer.length > C.BYTES.kib(64)) {
|
|
1721
|
+
throw new TypeError(
|
|
1722
|
+
"crypto.hashCertFingerprint: PEM input exceeds 64 KiB (" +
|
|
1723
|
+
pemOrDer.length + " bytes); refuse oversized input to avoid " +
|
|
1724
|
+
"polynomial-ReDoS on the BEGIN/END marker regex"
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1667
1727
|
var match = pemOrDer.match(/-----BEGIN [A-Z0-9 ]+-----([\s\S]+?)-----END [A-Z0-9 ]+-----/);
|
|
1668
1728
|
if (!match) {
|
|
1669
1729
|
throw new TypeError("crypto.hashCertFingerprint: PEM input lacks BEGIN/END markers");
|
|
@@ -244,7 +244,7 @@ function composePipeline(entries, opts) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
var pipelineId = bCrypto.namespaceHash("system.middleware.compose.pipeline",
|
|
247
|
-
resolved.map(function (r) { return r.name; }).join("\
|
|
247
|
+
resolved.map(function (r) { return r.name; }).join("\0"));
|
|
248
248
|
|
|
249
249
|
_emitAudit("system.middleware.compose.pipeline_built", {
|
|
250
250
|
pipelineId: pipelineId,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:65836635-3daf-43b2-804d-050f9f4a4082",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-17T05:24:59.072Z",
|
|
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.10.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.10.3",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.10.
|
|
25
|
+
"version": "0.10.3",
|
|
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.10.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.10.3",
|
|
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.10.
|
|
57
|
+
"ref": "@blamejs/core@0.10.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|