@blamejs/core 0.6.63 → 0.6.64

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.6.x
10
10
 
11
+ - **0.6.64** (2026-05-03) — `b.credentialHash.verify` now enforces the same 16-byte minimum payload that `hash()` enforces. Pre-fix the asymmetry was a real risk: `hash()` refused to *create* a hash shorter than 16 bytes (`length < 16` rejected with `credential-hash/bad-opt`), but `verify()` silently accepted any payload length, including 1-byte payloads where the collision space is **256** — a hand-crafted envelope `{ magic, algoId, SHAKE256(targetPassword, 1)[0] }` would verify against the target password in microseconds. A storage bug or attacker tampering that truncated the stored envelope produced a verifiable but catastrophically weak hash; an attacker with DB write access who couldn't replace the hash outright could *truncate* it to a nearly-cleartext-equivalent value. Both ends now refuse short payloads symmetrically via the new `SHAKE256_MIN_LENGTH = 16` constant; verify returns false (with `payload-too-short` observability label) when the envelope's payload is < 16 bytes. Legitimate truncation that keeps the payload ≥ 16 bytes still verifies (the framework's design intentionally supports XOF variable-length digests via `params: { length: N }`); the bound only refuses hashes too short to provide meaningful collision resistance. **Tests** — 4 new layer-0 assertions on `b.credentialHash.verify` (1-byte attack envelope rejected, 15-byte rejected, 16-byte accepts, hash() rejects length 15). Smoke 7226 → 7230 / wiki e2e 178 / Linux 86.5s / eslint clean / shellcheck clean.
12
+
11
13
  - **0.6.63** (2026-05-03) — `b.parsers.toml.parse` enforces `maxDepth` on dotted-key paths. Pre-fix the parser walked arbitrarily deep table headers (`[a.b.c.d.e…]`) without applying the existing `maxDepth` (which only ran inside `_parseValue` for inline tables / arrays); a 10,000-segment path built a tree deep enough to stack-overflow the recursive `_normalize` walker post-parse. An attacker submitting a malicious TOML config to `b.config` would crash the framework at boot or whenever the file was reloaded. Now `_parseDottedKey` rejects a path with more than `maxDepth` segments via the same `toml/too-deep` error code already used for value-side depth. **Tests** — 4 new layer-0 assertions: 99-segment path under default depth accepts, 150-segment path rejects with `toml/too-deep`, 10K-segment path rejects (no stack overflow), `maxDepth: 1000` opt allows a 500-segment path. Smoke 7222 → 7226 / wiki e2e 178 / Linux 85.6s / eslint clean / shellcheck clean.
12
14
 
13
15
  - **0.6.62** (2026-05-03) — URL-length DoS-shape gap closed in two places. **`b.safeSchema.string().url()`** was regex-only with no length cap, accepting arbitrarily long matching strings — same DoS class as v0.6.61's `.email()` gap; an operator chaining `.url()` on a request body would feed multi-megabyte URLs into downstream HTTP clients / SSRF gates / log lines. Now rejects with `string/url-too-long` at 8193+ chars. **`b.safeUrl.parse`** had the same gap at the framework's primary URL-validation surface (used by httpClient + ssrfGuard + every operator-supplied URL). The 8 KB cap now applies BEFORE handing the string to Node's `new URL()` parser; operator-supplied URLs feeding `b.httpClient.request({ url })` from request bodies / webhook configs are bounded. Operators with legitimate non-standard use (proxies, tunnels with embedded payloads) override via `opts.maxUrlLength`. Both bounds reference RFC 7230 §3.1.1's 8000-octet recommendation. **Tests** — 4 new layer-0 assertions on `string().url()` (8 KB / 8193 / 100K / error code) + 4 on `safeUrl.parse` (8192 accept / 8193 reject / error code / `maxUrlLength` opt override). Smoke 7215 → 7222 / wiki e2e 178 / Linux 85.5s / per-primitive integration 16 files / eslint clean / shellcheck clean.
@@ -93,6 +93,13 @@ function _emitEvent(name, value, labels) {
93
93
  // to the larger size; operators pass `{ params: { length: 64 } }`
94
94
  // to opt into the SHA3-512-comparable byte count if they need it.
95
95
  var SHAKE256_DEFAULT_LENGTH = 128;
96
+ // Minimum payload length both hash() and verify() enforce. 16 bytes
97
+ // (128 bits) is the floor below which the digest's collision space
98
+ // becomes brute-forceable. The asymmetry between hash() (rejected < 16
99
+ // at config time) and verify() (silently accepted any length) let a
100
+ // truncated stored envelope verify with trivial work; both ends now
101
+ // refuse short payloads symmetrically.
102
+ var SHAKE256_MIN_LENGTH = 16;
96
103
 
97
104
  function _shake256(secret, length) {
98
105
  // crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
@@ -200,9 +207,11 @@ async function hash(secret, opts) {
200
207
 
201
208
  if (algoId === C.CRED_HASH_IDS.SHAKE256) {
202
209
  var length = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
203
- if (typeof length !== "number" || !isFinite(length) || length < 16 || Math.floor(length) !== length) {
210
+ if (typeof length !== "number" || !isFinite(length) ||
211
+ length < SHAKE256_MIN_LENGTH || Math.floor(length) !== length) {
204
212
  throw new CredentialHashError(
205
- "credentialHash.hash: SHAKE256 length must be an integer >= 16, got " + JSON.stringify(length),
213
+ "credentialHash.hash: SHAKE256 length must be an integer >= " +
214
+ SHAKE256_MIN_LENGTH + ", got " + JSON.stringify(length),
206
215
  "credential-hash/bad-opt");
207
216
  }
208
217
  var env = _envelope(algoId, _shake256(secret, length));
@@ -246,6 +255,19 @@ async function verify(secret, envelope) {
246
255
  var algoName = ID_TO_NAME[decoded.algoId];
247
256
 
248
257
  if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
258
+ // Enforce the same minimum payload length that hash() enforces (16
259
+ // bytes / 128 bits). Without this, a storage bug or attacker
260
+ // tampering that truncates the stored envelope to just a few bytes
261
+ // produces a hash with catastrophically reduced collision space —
262
+ // a 1-byte payload has only 256 possible values, brute-forceable
263
+ // in microseconds. The asymmetry between hash() (rejects < 16) and
264
+ // verify() (accepted anything) was the silent gap; both ends now
265
+ // refuse weak digests symmetrically.
266
+ if (decoded.payload.length < SHAKE256_MIN_LENGTH) {
267
+ _emitEvent("credentialHash.verify", 1,
268
+ { outcome: "failure", reason: "payload-too-short", algo: algoName });
269
+ return false;
270
+ }
249
271
  var expected = _shake256(secret, decoded.payload.length);
250
272
  var ok = crypto.timingSafeEqual(expected, decoded.payload);
251
273
  _emitEvent("credentialHash.verify", 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.63",
3
+ "version": "0.6.64",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:192ac8a0-4291-44dd-a03a-44af6b4cce65",
5
+ "serialNumber": "urn:uuid:e636e4f9-23c0-41e8-8e78-f898e223c0e7",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-03T14:04:51.705Z",
8
+ "timestamp": "2026-05-03T14:18:03.806Z",
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.6.63",
22
+ "bom-ref": "@blamejs/core@0.6.64",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.63",
25
+ "version": "0.6.64",
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.6.63",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.64",
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.6.63",
57
+ "ref": "@blamejs/core@0.6.64",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]