@blamejs/core 0.15.67 → 0.15.68

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.
@@ -798,6 +798,68 @@ function stripCrlf(s, replacement) {
798
798
  return s.replace(CRLF_RE_GLOBAL, replacement === undefined ? "" : replacement);
799
799
  }
800
800
 
801
+ /**
802
+ * @primitive b.safeBuffer.foldHeaderText
803
+ * @signature b.safeBuffer.foldHeaderText(value, replacement?)
804
+ * @since 0.15.68
805
+ * @status stable
806
+ * @related b.safeBuffer.assertHeaderSafe, b.safeBuffer.stripCrlf
807
+ *
808
+ * Neutralize free-text bound for a CRLF-delimited protocol line: replace
809
+ * every CR and LF with <code>replacement</code> (default a single space) so
810
+ * the text folds onto one line, AND remove every NUL byte. Use this for a
811
+ * value that may LEGITIMATELY wrap — a multi-line SMTP 5xx reply folded into
812
+ * one diagnostic line — where <code>assertHeaderSafe</code> (reject) would
813
+ * be too strict. Unlike bare <code>stripCrlf</code>, this also strips NUL,
814
+ * which is never valid in an RFC 5322 header value and which downstream
815
+ * SMTP / mail parsers treat specially. Non-string input passes through
816
+ * unchanged.
817
+ *
818
+ * @example
819
+ * b.safeBuffer.foldHeaderText("550 mailbox full\r\nX-Injected: evil");
820
+ * // → "550 mailbox full X-Injected: evil"
821
+ */
822
+ function foldHeaderText(value, replacement) {
823
+ if (typeof value !== "string") return value;
824
+ var rep = replacement === undefined ? " " : replacement;
825
+ return value.replace(CRLF_RE_GLOBAL, rep).split("\u0000").join("");
826
+ }
827
+
828
+ /**
829
+ * @primitive b.safeBuffer.assertHeaderSafe
830
+ * @signature b.safeBuffer.assertHeaderSafe(value, label, ErrorClass, code)
831
+ * @since 0.15.68
832
+ * @status stable
833
+ * @related b.safeBuffer.hasCrlf, b.safeBuffer.stripCrlf
834
+ *
835
+ * Throw when a string bound for a CRLF-delimited protocol line — an SMTP /
836
+ * RFC 5322 header value, an HTTP header — contains CR, LF, or a NUL byte,
837
+ * the canonical header-injection / smuggling vector. Route every
838
+ * <code>Name: value\r\n</code> builder's STRUCTURED fields (addresses,
839
+ * domains, identifiers, MTA names) through this; they can never
840
+ * legitimately carry those bytes. For free-text that may legitimately wrap
841
+ * (a multi-line SMTP reply folded into one diagnostic line), fold it with
842
+ * <code>stripCrlf</code> instead of rejecting. Throws
843
+ * <code>new ErrorClass(code, ...)</code> so each caller reports in its own
844
+ * error domain (the <code>validateOpts</code> convention). A non-string
845
+ * value passes through untouched — callers type-check separately.
846
+ *
847
+ * @example
848
+ * b.safeBuffer.assertHeaderSafe("rcpt@example.com", "to", MailError, "mail/bad-header");
849
+ * // → "rcpt@example.com"
850
+ *
851
+ * b.safeBuffer.assertHeaderSafe("rcpt\r\nBcc: evil@x", "to", MailError, "mail/bad-header");
852
+ * // → throws MailError("mail/bad-header")
853
+ */
854
+ function assertHeaderSafe(value, label, ErrorClass, code) {
855
+ if (typeof value === "string" &&
856
+ (CRLF_RE.test(value) || value.indexOf("\u0000") !== -1)) {
857
+ throw new ErrorClass(code,
858
+ label + ": must not contain CR, LF, or NUL (header injection)");
859
+ }
860
+ return value;
861
+ }
862
+
801
863
  module.exports = {
802
864
  normalizeText: normalizeText,
803
865
  toBuffer: toBuffer,
@@ -809,6 +871,8 @@ module.exports = {
809
871
  isHex: isHex,
810
872
  hasCrlf: hasCrlf,
811
873
  stripCrlf: stripCrlf,
874
+ foldHeaderText: foldHeaderText,
875
+ assertHeaderSafe: assertHeaderSafe,
812
876
  stripTrailingHspace: stripTrailingHspace,
813
877
  indexAfterOpenTag: indexAfterOpenTag,
814
878
  HEX_RE: HEX_RE,
package/lib/safe-icap.js CHANGED
@@ -77,6 +77,7 @@
77
77
 
78
78
  var C = require("./constants");
79
79
  var { defineClass } = require("./framework-error");
80
+ var pick = require("./pick");
80
81
  var gateContract = require("./gate-contract");
81
82
 
82
83
  var SafeIcapError = defineClass("SafeIcapError", { alwaysPermanent: true });
@@ -395,6 +396,10 @@ function _parseHeaderLine(line, maxValueBytes) {
395
396
  }
396
397
 
397
398
  function _addHeader(headers, name, value) {
399
+ // A header named __proto__ / constructor / prototype would pollute or
400
+ // shadow the plain-object header map; drop it (ICAP headers are echoed
401
+ // from an untrusted upstream response).
402
+ if (pick.isPoisonedKey(name)) return;
398
403
  if (headers[name] === undefined) {
399
404
  headers[name] = value;
400
405
  } else if (Array.isArray(headers[name])) {
package/lib/session.js CHANGED
@@ -510,6 +510,30 @@ async function create(opts) {
510
510
  * var roles = (info.data && info.data.roles) || [];
511
511
  * // → { userId: "user-42", data: { roles: ["admin"] }, createdAt: ..., expiresAt: ..., lastActivity: ..., fingerprintDrift: false, fingerprintAnomalyScore: null }
512
512
  */
513
+ // Evaluate the idle + absolute timeout floors against a session row.
514
+ // Returns { action, metadata } describing the breach, or null if the session
515
+ // is within both floors. Centralized so EVERY session-read path (verify,
516
+ // touch, ...) enforces the floors identically — a refresh must never
517
+ // resurrect a session that verify() would expire.
518
+ function _timeoutFloorBreach(row, nowMs, opts) {
519
+ opts = opts || {};
520
+ var idleMs = opts.idleTimeoutMs !== undefined ? opts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
521
+ var absMs = opts.absoluteTimeoutMs !== undefined ? opts.absoluteTimeoutMs : DEFAULT_ABSOLUTE_TIMEOUT_MS;
522
+ if (idleMs > 0) {
523
+ var lastActivity = Number(row.lastActivity);
524
+ if ((nowMs - lastActivity) > idleMs) {
525
+ return { action: "auth.session.expired_idle", metadata: { idleMs: nowMs - lastActivity, threshold: idleMs } };
526
+ }
527
+ }
528
+ if (absMs > 0) {
529
+ var createdAt = Number(row.createdAt);
530
+ if ((nowMs - createdAt) > absMs) {
531
+ return { action: "auth.session.expired_absolute", metadata: { ageMs: nowMs - createdAt, threshold: absMs } };
532
+ }
533
+ }
534
+ return null;
535
+ }
536
+
513
537
  async function verify(token, verifyOpts) {
514
538
  if (typeof token !== "string" || token.length === 0) return null;
515
539
  verifyOpts = verifyOpts || {};
@@ -541,39 +565,15 @@ async function verify(token, verifyOpts) {
541
565
  // SP 800-63B-4). These shorten the effective lifetime even when the
542
566
  // operator picked a long ttlMs. Defaults: idle 30m, absolute 12h.
543
567
  // Operator opt-out by passing 0 (disables that timeout).
544
- var idleMs = verifyOpts.idleTimeoutMs !== undefined
545
- ? verifyOpts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
546
- var absMs = verifyOpts.absoluteTimeoutMs !== undefined
547
- ? verifyOpts.absoluteTimeoutMs : DEFAULT_ABSOLUTE_TIMEOUT_MS;
548
- if (idleMs > 0) {
549
- var lastActivity = Number(row.lastActivity);
550
- if ((nowMs - lastActivity) > idleMs) {
551
- try {
552
- audit.safeEmit({
553
- action: "auth.session.expired_idle", outcome: "success",
554
- metadata: { idleMs: nowMs - lastActivity, threshold: idleMs },
555
- });
556
- } catch (_ignored) { /* audit best-effort */ }
557
- if (cluster.isLeader()) {
558
- try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
559
- }
560
- return null;
561
- }
562
- }
563
- if (absMs > 0) {
564
- var createdAt = Number(row.createdAt);
565
- if ((nowMs - createdAt) > absMs) {
566
- try {
567
- audit.safeEmit({
568
- action: "auth.session.expired_absolute", outcome: "success",
569
- metadata: { ageMs: nowMs - createdAt, threshold: absMs },
570
- });
571
- } catch (_ignored) { /* audit best-effort */ }
572
- if (cluster.isLeader()) {
573
- try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
574
- }
575
- return null;
568
+ var floorBreach = _timeoutFloorBreach(row, nowMs, verifyOpts);
569
+ if (floorBreach) {
570
+ try {
571
+ audit.safeEmit({ action: floorBreach.action, outcome: "success", metadata: floorBreach.metadata });
572
+ } catch (_ignored) { /* audit best-effort */ }
573
+ if (cluster.isLeader()) {
574
+ try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
576
575
  }
576
+ return null;
577
577
  }
578
578
  // Unseal sealed columns (userId, data) using the cryptoField pipeline
579
579
  // so we return cleartext to the caller — same shape as the previous
@@ -909,6 +909,21 @@ async function touch(token, opts) {
909
909
  if (sid === null) return false;
910
910
  var sidHash = _hashSid(sid);
911
911
  var nowMs = Date.now();
912
+ // A session past its idle / absolute timeout floor must not be resurrected
913
+ // by a refresh — enforce the floors (as verify does) before extending.
914
+ // Without this, touch() would reset lastActivity on a session verify()
915
+ // would have expired, defeating the floor.
916
+ var floorSel = sql.select(_sessionSqlTable(), _sessionSqlOpts())
917
+ .columns(["createdAt", "lastActivity"])
918
+ .where("sidHash", sidHash)
919
+ .where("expiresAt", ">=", nowMs)
920
+ .toSql();
921
+ var floorRow = await _currentStore().executeOne(floorSel.sql, floorSel.params);
922
+ if (!floorRow) return false;
923
+ if (_timeoutFloorBreach(floorRow, nowMs, opts)) {
924
+ try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
925
+ return false;
926
+ }
912
927
  // Two SQL paths so the SET list stays static (no dynamic column
913
928
  // assembly) and matches the call shape clusterStorage expects.
914
929
  // extendBy resets expiresAt relative to NOW, not relative to the
package/lib/vc.js CHANGED
@@ -69,6 +69,32 @@ var JOSE_ALGS = {
69
69
  "EdDSA": { nodeHash: null },
70
70
  };
71
71
 
72
+ // RFC 7518 §3.4 binds each ECDSA alg to a specific curve. The verifier must
73
+ // reject a signature whose header alg does not match the resolved key's type
74
+ // / curve — otherwise the attacker picks the alg (hash + encoding) from the
75
+ // header independent of the key an ECDSA curve/type confusion (CWE-347).
76
+ var _ES_ALG_CURVE = { "ES256": "prime256v1", "ES384": "secp384r1", "ES512": "secp521r1" };
77
+
78
+ function _assertJoseAlgKey(algName, key) {
79
+ var kt = key && key.asymmetricKeyType;
80
+ if (algName === "EdDSA") {
81
+ if (kt !== "ed25519" && kt !== "ed448") {
82
+ throw new VcError("vc/alg-key-mismatch",
83
+ "vc.verify: alg EdDSA requires an Ed25519/Ed448 key, got " + (kt || "?"));
84
+ }
85
+ return;
86
+ }
87
+ var wantCurve = _ES_ALG_CURVE[algName];
88
+ if (wantCurve) {
89
+ var gotCurve = key && key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve;
90
+ if (kt !== "ec" || gotCurve !== wantCurve) {
91
+ throw new VcError("vc/alg-key-mismatch",
92
+ "vc.verify: alg " + algName + " requires an EC " + wantCurve + " key, got " +
93
+ (kt || "?") + "/" + (gotCurve || "?"));
94
+ }
95
+ }
96
+ }
97
+
72
98
  function _b64urlJson(obj) {
73
99
  return Buffer.from(JSON.stringify(obj), "utf8").toString("base64url");
74
100
  }
@@ -234,6 +260,9 @@ function _verifyJose(token, opts, expectedTyp) {
234
260
  }
235
261
  var params = JOSE_ALGS[header.alg];
236
262
  var pub = opts.publicKey ? _toKey(opts.publicKey, "public") : _toKey(opts.keyResolver(header), "public");
263
+ // Bind the header-declared alg to the resolved key's type/curve before
264
+ // verifying — reject an alg the attacker chose that doesn't match the key.
265
+ _assertJoseAlgKey(header.alg, pub);
237
266
  var signingInput = parts[0] + "." + parts[1];
238
267
  var sig = Buffer.from(parts[2], "base64url");
239
268
  var ok = params.nodeHash === null
@@ -18,7 +18,7 @@
18
18
  "hashes": {
19
19
  "server": "sha256:5d539dfc9ef47121d4c09bd7256d76448a1f5ac47ee09ac44c78ff6a062af9ab"
20
20
  },
21
- "refreshedAt": "2026-06-25T02:57:00.483Z"
21
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
22
22
  },
23
23
  "@noble/curves": {
24
24
  "version": "2.2.0",
@@ -40,7 +40,7 @@
40
40
  "hashes": {
41
41
  "server": "sha256:ebf254d5eb56aef8705a1c4af9603f47987b4870a9bb5e657e06907b701e2731"
42
42
  },
43
- "refreshedAt": "2026-06-25T02:57:00.483Z"
43
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
44
44
  },
45
45
  "@noble/post-quantum": {
46
46
  "version": "0.6.1",
@@ -71,7 +71,7 @@
71
71
  "hashes": {
72
72
  "server": "sha256:f9190309daadca4c2e2cc2b76beaa6b96e463429cc3c390bd9f0ceaf7b588c68"
73
73
  },
74
- "refreshedAt": "2026-06-25T02:57:00.483Z"
74
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
75
75
  },
76
76
  "@simplewebauthn/server": {
77
77
  "version": "13.3.2",
@@ -94,7 +94,7 @@
94
94
  "hashes": {
95
95
  "server": "sha256:49411d893f5e9b0e2fcaa564b4ec7921f73a9a06229b5e53d49c1453ea1a365c"
96
96
  },
97
- "refreshedAt": "2026-06-25T02:57:00.483Z"
97
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
98
98
  },
99
99
  "SecLists-common-passwords-top-10000": {
100
100
  "version": "10k-most-common (master)",
@@ -114,7 +114,7 @@
114
114
  },
115
115
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
116
116
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
117
- "refreshedAt": "2026-06-25T02:57:00.483Z"
117
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
118
118
  },
119
119
  "bimi-trust-anchors": {
120
120
  "version": "operator-managed",
@@ -139,7 +139,7 @@
139
139
  },
140
140
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
141
141
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
142
- "refreshedAt": "2026-06-25T02:57:00.483Z"
142
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
143
143
  },
144
144
  "publicsuffix-list": {
145
145
  "version": "master",
@@ -154,12 +154,12 @@
154
154
  "bundler": "curl https://publicsuffix.org/list/public_suffix_list.dat",
155
155
  "bundledAt": "2026-06-24T00:00:00Z",
156
156
  "hashes": {
157
- "server": "sha256:42c4f3544726fa2feb4f304c889c5e391feb5d28080c9b6356ff82e267a93706",
158
- "data_js": "sha256:c840d9ae5c6bf4a07967340a5c649df2c6a66a287db41e408461c46cc32348e6"
157
+ "server": "sha256:5afbeea28737514a69a0bdca7cd3818979e67c443444de4d19f2f8713cf5372e",
158
+ "data_js": "sha256:6ed61937db12f148494db7c336498740bf4458cd9f49ec73d887eda83529f207"
159
159
  },
160
160
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
161
161
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
162
- "refreshedAt": "2026-06-25T02:57:00.483Z"
162
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
163
163
  },
164
164
  "peculiar-pki": {
165
165
  "version": "2.0.0+pkijs-3.4.0",
@@ -190,7 +190,7 @@
190
190
  "hashes": {
191
191
  "server": "sha256:9bbc191afaaa2b1e5757f00480457c08134cdc2c55d541df18d9155bba9cbf77"
192
192
  },
193
- "refreshedAt": "2026-06-25T02:57:00.483Z"
193
+ "refreshedAt": "2026-07-02T23:54:12.403Z"
194
194
  }
195
195
  }
196
196
  }
@@ -5,8 +5,8 @@
5
5
  // Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat,
6
6
  // rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported.
7
7
 
8
- // VERSION: 2026-06-24_06-18-09_UTC
9
- // COMMIT: 18ecca5d54471f21918798da451dd8d03a18f3c7
8
+ // VERSION: 2026-07-02_09-58-14_UTC
9
+ // COMMIT: d67b6cb8e70e9c76b0803809b66e7ffdd09484bd
10
10
 
11
11
  // Instructions on pulling and using this list can be found at https://publicsuffix.org/list/.
12
12
 
@@ -14614,7 +14614,6 @@ polyspace.com
14614
14614
  // May First - People Link : https://mayfirst.org/
14615
14615
  // Submitted by Jamie McClelland <info@mayfirst.org>
14616
14616
  mayfirst.info
14617
- mayfirst.org
14618
14617
 
14619
14618
  // McHost : https://mchost.ru
14620
14619
  // Submitted by Evgeniy Subbotin <e.subbotin@mchost.ru>
@@ -16411,7 +16410,6 @@ zabc.net
16411
16410
 
16412
16411
  // ===END PRIVATE DOMAINS===
16413
16412
 
16414
-
16415
16413
  // ===BEGIN blamejs canary===
16416
16414
  // Honeytoken — vendor-data integrity defense (lib/vendor-data.js).
16417
16415
  _blamejs_canary_v0_9_8_.local