@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 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. Coerces non-Buffer inputs via
403
- * `Buffer.from(String(...))`, returns `false` immediately when lengths
404
- * differ (length itself is not a secret), then routes equal-length
405
- * inputs through `crypto.timingSafeEqual`. Use when comparing HMAC
406
- * digests, session tokens, password-reset codes, or any
407
- * attacker-influenced value where a timing oracle would leak bits.
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
- var bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
417
- var bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
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("\n"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:c1165275-044f-4a5a-b7dc-061727bfd076",
5
+ "serialNumber": "urn:uuid:65836635-3daf-43b2-804d-050f9f4a4082",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-16T23:28:39.659Z",
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.2",
22
+ "bom-ref": "@blamejs/core@0.10.3",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.10.2",
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.2",
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.2",
57
+ "ref": "@blamejs/core@0.10.3",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]