@blamejs/core 0.15.13 → 0.15.15

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.
Files changed (208) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +38 -6
  4. package/lib/agent-event-bus.js +13 -0
  5. package/lib/agent-idempotency.js +5 -1
  6. package/lib/agent-snapshot.js +32 -2
  7. package/lib/ai-aedt-bias-audit.js +2 -1
  8. package/lib/ai-content-detect.js +1 -3
  9. package/lib/ai-frontier-protocol.js +1 -1
  10. package/lib/ai-model-manifest.js +1 -1
  11. package/lib/ai-output.js +16 -7
  12. package/lib/api-snapshot.js +4 -1
  13. package/lib/app-shutdown.js +7 -1
  14. package/lib/archive-gz.js +9 -0
  15. package/lib/archive-read.js +9 -7
  16. package/lib/archive-tar-read.js +51 -8
  17. package/lib/archive.js +4 -2
  18. package/lib/asn1-der.js +70 -22
  19. package/lib/atomic-file.js +204 -2
  20. package/lib/audit-chain.js +54 -5
  21. package/lib/audit-daily-review.js +12 -2
  22. package/lib/audit-sign.js +2 -1
  23. package/lib/audit-tools.js +108 -23
  24. package/lib/audit.js +7 -2
  25. package/lib/auth/access-lock.js +2 -2
  26. package/lib/auth/bot-challenge.js +1 -1
  27. package/lib/auth/ciba.js +71 -8
  28. package/lib/auth/dpop.js +15 -1
  29. package/lib/auth/fido-mds3.js +30 -13
  30. package/lib/auth/jwt.js +21 -5
  31. package/lib/auth/lockout.js +18 -2
  32. package/lib/auth/oauth.js +8 -2
  33. package/lib/auth/passkey.js +1 -1
  34. package/lib/auth/password.js +1 -1
  35. package/lib/auth/saml.js +42 -12
  36. package/lib/auth/sd-jwt-vc.js +24 -4
  37. package/lib/auth/status-list.js +14 -2
  38. package/lib/auth/step-up.js +9 -1
  39. package/lib/auth-bot-challenge.js +21 -2
  40. package/lib/backup/bundle.js +7 -2
  41. package/lib/backup/crypto.js +23 -8
  42. package/lib/backup/index.js +41 -20
  43. package/lib/backup/manifest.js +7 -1
  44. package/lib/break-glass.js +41 -22
  45. package/lib/cbor.js +34 -11
  46. package/lib/cdn-cache-control.js +7 -3
  47. package/lib/cert.js +5 -3
  48. package/lib/cli.js +5 -1
  49. package/lib/cloud-events.js +3 -2
  50. package/lib/cluster-storage.js +7 -3
  51. package/lib/codepoint-class.js +17 -0
  52. package/lib/compliance-eaa.js +1 -1
  53. package/lib/compliance-sanctions.js +9 -7
  54. package/lib/compliance.js +1 -1
  55. package/lib/config-drift.js +22 -8
  56. package/lib/content-credentials.js +11 -8
  57. package/lib/content-digest.js +11 -4
  58. package/lib/cookies.js +10 -2
  59. package/lib/cose.js +20 -0
  60. package/lib/crdt.js +2 -1
  61. package/lib/crypto-field.js +48 -24
  62. package/lib/crypto.js +18 -4
  63. package/lib/csp.js +13 -0
  64. package/lib/daemon.js +4 -1
  65. package/lib/data-act.js +27 -4
  66. package/lib/db-file-lifecycle.js +14 -6
  67. package/lib/db-query.js +102 -11
  68. package/lib/db.js +32 -15
  69. package/lib/dora.js +43 -13
  70. package/lib/dr-runbook.js +1 -1
  71. package/lib/dsa.js +2 -1
  72. package/lib/dsr.js +22 -8
  73. package/lib/early-hints.js +19 -0
  74. package/lib/eat.js +5 -1
  75. package/lib/external-db.js +60 -4
  76. package/lib/fda-21cfr11.js +30 -5
  77. package/lib/flag-providers.js +6 -2
  78. package/lib/forms.js +1 -1
  79. package/lib/gate-contract.js +46 -5
  80. package/lib/gdpr-ropa.js +18 -9
  81. package/lib/graphql-federation.js +17 -4
  82. package/lib/guard-all.js +2 -2
  83. package/lib/guard-dsn.js +1 -1
  84. package/lib/guard-envelope.js +1 -1
  85. package/lib/guard-html.js +9 -11
  86. package/lib/guard-imap-command.js +1 -1
  87. package/lib/guard-jmap.js +1 -1
  88. package/lib/guard-json.js +14 -6
  89. package/lib/guard-mail-move.js +1 -1
  90. package/lib/guard-managesieve-command.js +1 -1
  91. package/lib/guard-pop3-command.js +1 -1
  92. package/lib/guard-smtp-command.js +1 -1
  93. package/lib/guard-svg.js +8 -9
  94. package/lib/html-balance.js +7 -3
  95. package/lib/http-client-cookie-jar.js +33 -12
  96. package/lib/http-client.js +225 -53
  97. package/lib/iab-tcf.js +3 -2
  98. package/lib/importmap-integrity.js +41 -1
  99. package/lib/incident-report.js +9 -6
  100. package/lib/json-patch.js +1 -1
  101. package/lib/json-path.js +24 -3
  102. package/lib/jtd.js +2 -2
  103. package/lib/legal-hold.js +24 -8
  104. package/lib/log.js +2 -2
  105. package/lib/mail-agent.js +2 -2
  106. package/lib/mail-arf.js +1 -1
  107. package/lib/mail-auth.js +27 -4
  108. package/lib/mail-bimi.js +16 -16
  109. package/lib/mail-bounce.js +3 -3
  110. package/lib/mail-crypto-smime.js +71 -6
  111. package/lib/mail-deploy.js +9 -5
  112. package/lib/mail-dkim.js +20 -7
  113. package/lib/mail-greylist.js +2 -4
  114. package/lib/mail-helo.js +2 -4
  115. package/lib/mail-journal.js +11 -8
  116. package/lib/mail-mdn.js +8 -4
  117. package/lib/mail-rbl.js +2 -4
  118. package/lib/mail-scan.js +3 -5
  119. package/lib/mail-server-jmap.js +4 -1
  120. package/lib/mail-server-registry.js +1 -1
  121. package/lib/mail-server-tls.js +9 -2
  122. package/lib/mail-spam-score.js +2 -4
  123. package/lib/mail-store-fts.js +1 -1
  124. package/lib/mail.js +22 -2
  125. package/lib/markup-tokenizer.js +24 -0
  126. package/lib/mcp.js +6 -4
  127. package/lib/mdoc.js +26 -3
  128. package/lib/metrics.js +14 -3
  129. package/lib/middleware/api-encrypt.js +2 -2
  130. package/lib/middleware/body-parser.js +10 -4
  131. package/lib/middleware/bot-guard.js +26 -18
  132. package/lib/middleware/clear-site-data.js +5 -1
  133. package/lib/middleware/compose-pipeline.js +39 -5
  134. package/lib/middleware/compression.js +9 -0
  135. package/lib/middleware/cors.js +32 -23
  136. package/lib/middleware/csrf-protect.js +60 -21
  137. package/lib/middleware/daily-byte-quota.js +6 -4
  138. package/lib/middleware/fetch-metadata.js +28 -4
  139. package/lib/middleware/network-allowlist.js +61 -30
  140. package/lib/middleware/rate-limit.js +25 -16
  141. package/lib/middleware/scim-server.js +2 -1
  142. package/lib/middleware/security-headers.js +24 -6
  143. package/lib/middleware/speculation-rules.js +6 -3
  144. package/lib/middleware/tus-upload.js +2 -2
  145. package/lib/money.js +1 -1
  146. package/lib/mtls-ca.js +10 -6
  147. package/lib/network-dns-resolver.js +1 -1
  148. package/lib/network-dns.js +9 -2
  149. package/lib/network-dnssec.js +2 -1
  150. package/lib/network-smtp-policy.js +23 -5
  151. package/lib/network-tls.js +27 -5
  152. package/lib/network-tsig.js +2 -2
  153. package/lib/nis2-report.js +1 -1
  154. package/lib/nist-crosswalk.js +1 -1
  155. package/lib/ntp-check.js +28 -0
  156. package/lib/numeric-bounds.js +9 -0
  157. package/lib/object-store/azure-blob.js +1 -2
  158. package/lib/object-store/gcs-bucket-ops.js +4 -2
  159. package/lib/object-store/gcs.js +6 -4
  160. package/lib/object-store/http-put.js +1 -2
  161. package/lib/object-store/http-request.js +30 -1
  162. package/lib/object-store/local.js +37 -17
  163. package/lib/object-store/sigv4.js +1 -2
  164. package/lib/observability-otlp-exporter.js +20 -4
  165. package/lib/outbox.js +11 -4
  166. package/lib/parsers/safe-xml.js +1 -1
  167. package/lib/parsers/safe-yaml.js +21 -3
  168. package/lib/pipl-cn.js +11 -8
  169. package/lib/queue-local.js +10 -3
  170. package/lib/redact.js +7 -3
  171. package/lib/request-helpers.js +347 -36
  172. package/lib/resource-access-lock.js +3 -3
  173. package/lib/restore-bundle.js +46 -18
  174. package/lib/restore-rollback.js +10 -4
  175. package/lib/restore.js +19 -0
  176. package/lib/retention.js +20 -4
  177. package/lib/router.js +17 -4
  178. package/lib/safe-ical.js +2 -2
  179. package/lib/safe-icap.js +1 -1
  180. package/lib/safe-json.js +70 -0
  181. package/lib/safe-sieve.js +1 -1
  182. package/lib/safe-vcard.js +1 -1
  183. package/lib/sandbox-worker.js +6 -0
  184. package/lib/sandbox.js +1 -1
  185. package/lib/scheduler.js +17 -1
  186. package/lib/self-update-standalone-verifier.js +16 -0
  187. package/lib/session.js +62 -120
  188. package/lib/sql.js +25 -3
  189. package/lib/static.js +65 -13
  190. package/lib/template.js +7 -5
  191. package/lib/tenant-quota.js +52 -19
  192. package/lib/tsa.js +5 -2
  193. package/lib/vault/index.js +5 -0
  194. package/lib/vault/passphrase-ops.js +22 -26
  195. package/lib/vault/passphrase-source.js +8 -3
  196. package/lib/vault/rotate.js +13 -18
  197. package/lib/vault/seal-pem-file.js +4 -1
  198. package/lib/vc.js +1 -1
  199. package/lib/vendor/MANIFEST.json +10 -10
  200. package/lib/vendor/public-suffix-list.dat +23 -10
  201. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  202. package/lib/webhook.js +16 -1
  203. package/lib/websocket.js +1 -1
  204. package/lib/worm.js +1 -1
  205. package/lib/ws-client.js +83 -46
  206. package/lib/x509-chain.js +91 -0
  207. package/package.json +1 -1
  208. package/sbom.cdx.json +6 -6
@@ -1138,7 +1138,10 @@ function create(opts) {
1138
1138
  return;
1139
1139
  }
1140
1140
  var text = data.toString("utf8");
1141
- if (text.length > (opts.webSocketMaxMessageBytes || (10 * 1024 * 1024))) { // allow:raw-byte-literal — mirrors handleUpgrade cap
1141
+ // Byte cap measured on the raw frame buffer, not text.length (UTF-16
1142
+ // code units) — a multibyte payload is 2-4x larger in bytes than chars,
1143
+ // so a char-length check under-enforces the limit.
1144
+ if (data.length > (opts.webSocketMaxMessageBytes || (10 * 1024 * 1024))) { // allow:raw-byte-literal — mirrors handleUpgrade cap
1142
1145
  _sendRequestError(null,
1143
1146
  "urn:ietf:params:jmap:error:limit",
1144
1147
  "WebSocket message exceeds maxSizeRequest");
@@ -142,7 +142,7 @@ function create(opts) {
142
142
  validateOpts.requireNonEmptyString(opts.protocol,
143
143
  "b.mail.serverRegistry.create: protocol", MailServerRegistryError,
144
144
  "mail-server-registry/bad-protocol");
145
- if (!CATALOGUE[opts.protocol]) {
145
+ if (!Object.prototype.hasOwnProperty.call(CATALOGUE, opts.protocol)) {
146
146
  throw new MailServerRegistryError("mail-server-registry/unknown-protocol",
147
147
  "create: protocol must be 'imap', 'jmap', or 'managesieve' (got '" + opts.protocol + "')");
148
148
  }
@@ -93,6 +93,7 @@ var nodeFs = require("node:fs");
93
93
  var nodeTls = require("node:tls");
94
94
  var lazyRequire = require("./lazy-require");
95
95
  var C = require("./constants");
96
+ var atomicFile = require("./atomic-file");
96
97
  var validateOpts = require("./validate-opts");
97
98
  var { defineClass } = require("./framework-error");
98
99
 
@@ -177,7 +178,11 @@ function context(opts) {
177
178
  var stopped = false;
178
179
 
179
180
  function _readKey() {
180
- var raw = nodeFs.readFileSync(keyFile, "utf8");
181
+ // Cap + fd-bound TLS-private-key read. NO refuseSymlink: keyFile is commonly
182
+ // certbot's /etc/letsencrypt/live/<host>/privkey.pem, a symlink chain into
183
+ // ../../archive — refusing symlinks would break the documented Let's Encrypt
184
+ // layout. The cap is the OOM-before-use defense.
185
+ var raw = atomicFile.fdSafeReadSync(keyFile, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
181
186
  // b.vault.sealPemFile produces blobs that decrypt via vault.unseal.
182
187
  // Detect by the sealed-cell prefix the framework's vault layer
183
188
  // already documents (everything else passes through as plain PEM).
@@ -196,7 +201,9 @@ function context(opts) {
196
201
  function _build() {
197
202
  var certPem;
198
203
  try {
199
- certPem = nodeFs.readFileSync(certFile, "utf8");
204
+ // Cap + fd-bound cert-chain read. NO refuseSymlink (certbot fullchain.pem
205
+ // is a symlink); the cap is the safe hardening.
206
+ certPem = atomicFile.fdSafeReadSync(certFile, { maxBytes: C.BYTES.mib(1), encoding: "utf8" });
200
207
  } catch (e) {
201
208
  throw new MailServerTlsError("mail-server-tls/cert-unreadable",
202
209
  "b.mail.server.tls.context: cannot read certFile " + certFile + ": " +
@@ -133,7 +133,7 @@ function create(opts) {
133
133
  "mail.spamScore.create.scorer must be a function; got " + (typeof opts.scorer));
134
134
  }
135
135
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
136
- if (!PROFILES[profile]) {
136
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
137
137
  throw new MailSpamScoreError("mail-spam-score/bad-profile",
138
138
  "mail.spamScore.create.profile: unknown '" + profile +
139
139
  "' (valid: strict / balanced / permissive)");
@@ -222,9 +222,7 @@ function create(opts) {
222
222
  * @example
223
223
  * b.mail.spamScore.compliancePosture("hipaa"); // → "strict"
224
224
  */
225
- function compliancePosture(posture) {
226
- return COMPLIANCE_POSTURES[posture] || null;
227
- }
225
+ var compliancePosture = gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
228
226
 
229
227
  function _sanitizeReasons(reasons, caps) {
230
228
  if (reasons === undefined || reasons === null) return [];
@@ -321,7 +321,7 @@ var QUERY_KEY_MAP = {
321
321
  * // → { column: "addr_toks", field: "addr" }
322
322
  */
323
323
  function columnAndFieldFor(key) {
324
- return QUERY_KEY_MAP[key] || null;
324
+ return Object.prototype.hasOwnProperty.call(QUERY_KEY_MAP, key) ? QUERY_KEY_MAP[key] : null;
325
325
  }
326
326
 
327
327
  // Rewrite an operator query term into a FTS5 MATCH expression. The
package/lib/mail.js CHANGED
@@ -430,6 +430,26 @@ function _validateMessage(message) {
430
430
  throw new MailError("mail/invalid-subject",
431
431
  "message.subject contains forbidden CRLF", true);
432
432
  }
433
+ // Reply-To and custom header KEYS go straight onto the wire in _buildRfc822;
434
+ // a CRLF in either smuggles arbitrary headers (Bcc / Reply-To override /
435
+ // Content-Type) — RFC 5322 / CWE-93 header injection. Fail closed.
436
+ if (message.replyTo && safeBuffer.hasCrlf(String(message.replyTo))) {
437
+ throw new MailError("mail/invalid-reply-to",
438
+ "message.replyTo contains forbidden CRLF (header injection)", true);
439
+ }
440
+ if (message.headers && typeof message.headers === "object") {
441
+ for (var _hk in message.headers) {
442
+ if (!Object.prototype.hasOwnProperty.call(message.headers, _hk)) continue;
443
+ if (safeBuffer.hasCrlf(_hk) || _hk.indexOf("\0") !== -1) {
444
+ throw new MailError("mail/invalid-header",
445
+ "message.headers key contains forbidden CRLF/NUL (header injection)", true);
446
+ }
447
+ if (safeBuffer.hasCrlf(String(message.headers[_hk]))) {
448
+ throw new MailError("mail/invalid-header",
449
+ "message.headers value for '" + _hk + "' contains forbidden CRLF (header injection)", true);
450
+ }
451
+ }
452
+ }
433
453
 
434
454
  if (!message.text && !message.html && !message.calendar) {
435
455
  throw new MailError("mail/missing-body",
@@ -711,7 +731,7 @@ function _buildRfc822(message) {
711
731
  headers.push("From: " + message.from);
712
732
  headers.push("To: " + (Array.isArray(message.to) ? message.to.join(", ") : message.to));
713
733
  if (message.cc) headers.push("Cc: " + (Array.isArray(message.cc) ? message.cc.join(", ") : message.cc));
714
- if (message.replyTo) headers.push("Reply-To: " + message.replyTo);
734
+ if (message.replyTo) headers.push("Reply-To: " + safeBuffer.stripCrlf(String(message.replyTo)));
715
735
  if (message.subject) headers.push("Subject: " + message.subject);
716
736
  headers.push("MIME-Version: 1.0");
717
737
  headers.push("Date: " + new Date().toUTCString());
@@ -721,7 +741,7 @@ function _buildRfc822(message) {
721
741
  // Strip CRLF defensively even though we already validated the
722
742
  // message; custom headers go straight onto the wire.
723
743
  var v = safeBuffer.stripCrlf(String(message.headers[k]));
724
- headers.push(k + ": " + v);
744
+ headers.push(safeBuffer.stripCrlf(String(k)) + ": " + v);
725
745
  }
726
746
  }
727
747
  }
@@ -48,7 +48,31 @@ function splitTagNameAttrs(inner, tagNameRe) {
48
48
  };
49
49
  }
50
50
 
51
+ // htmlCommentEnd(s, lt) — given that an HTML comment opens at index `lt`
52
+ // (s.startsWith("<!--", lt)), return the index ONE PAST the comment's
53
+ // terminator per the WHATWG HTML tokenizer, not just the legacy "-->" form.
54
+ // A browser also closes a comment at "--!>" (comment-end-bang state) and
55
+ // ABRUPTLY closes one that begins "<!-->" or "<!--->". A scanner that honours
56
+ // only "-->" therefore disagrees with the browser about where the comment
57
+ // ends, so markup AFTER an early "--!>" / abrupt close is swallowed as inert
58
+ // comment by the sanitizer but parsed as a LIVE element by the browser (mXSS,
59
+ // the comment-parser differential). Returns -1 if the comment is unterminated
60
+ // so each caller keeps its own policy (lenient end-of-input vs. fail-closed
61
+ // throw). NOTE: HTML/SVG-in-HTML only — XML comments do NOT have these forms.
62
+ function htmlCommentEnd(s, lt) {
63
+ var i = lt + 4; // first char after "<!--"
64
+ if (s.charAt(i) === ">") return i + 1; // <!--> abrupt close
65
+ if (s.charAt(i) === "-" && s.charAt(i + 1) === ">") return i + 2; // <!---> abrupt close
66
+ var a = s.indexOf("-->", i);
67
+ var b = s.indexOf("--!>", i);
68
+ if (a === -1 && b === -1) return -1; // unterminated
69
+ if (a === -1) return b + 4;
70
+ if (b === -1) return a + 3;
71
+ return a <= b ? a + 3 : b + 4; // earliest terminator wins
72
+ }
73
+
51
74
  module.exports = {
52
75
  scanToTagEnd: scanToTagEnd,
53
76
  splitTagNameAttrs: splitTagNameAttrs,
77
+ htmlCommentEnd: htmlCommentEnd,
54
78
  };
package/lib/mcp.js CHANGED
@@ -636,10 +636,12 @@ function _validateValueAgainstSchema(value, schema, path) {
636
636
  }
637
637
  if (typeof schema.pattern === "string") {
638
638
  // Schema-supplied pattern — operator-controlled at registration
639
- // time, not request-controlled. Cap value length first per the
640
- // codebase-patterns regex-bound rule so a 10MB string doesn't
641
- // ReDoS the validator.
642
- if (value.length > 4096) return path + ": value exceeds 4 KiB cap before regex test"; // 4 KiB regex-input cap
639
+ // time, not request-controlled. Cap the input LENGTH first per the
640
+ // codebase-patterns regex-bound rule so a huge string can't ReDoS the
641
+ // validator. This is a CHARACTER cap, not a byte cap: regex matching
642
+ // cost scales with the number of code units the engine scans, so 4096
643
+ // chars is the correct ReDoS bound regardless of UTF-8 byte size.
644
+ if (value.length > 4096) return path + ": value exceeds 4096-char cap before regex test"; // ReDoS char cap (not bytes)
643
645
  try {
644
646
  var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern from registered tool author, not request input; bounded above
645
647
  if (!pat.test(value)) return path + ": does not match pattern";
package/lib/mdoc.js CHANGED
@@ -53,6 +53,7 @@ var C = require("./constants");
53
53
  var cbor = require("./cbor");
54
54
  var cose = require("./cose");
55
55
  var bCrypto = require("./crypto");
56
+ var x509Chain = require("./x509-chain");
56
57
  var safeBuffer = require("./safe-buffer");
57
58
  var validateOpts = require("./validate-opts");
58
59
  var { defineClass } = require("./framework-error");
@@ -128,10 +129,26 @@ function _mapGet(m, k) { return m instanceof Map ? m.get(k) : (m ? m[k] : undefi
128
129
  */
129
130
  async function verifyIssuerSigned(issuerSigned, opts) {
130
131
  validateOpts.requireObject(opts, "mdoc.verifyIssuerSigned", MdocError);
131
- validateOpts(opts, ["algorithms", "trustAnchorsPem", "expectedDocType", "at", "maxBytes", "maxDepth"], "mdoc.verifyIssuerSigned");
132
+ validateOpts(opts, ["algorithms", "trustAnchorsPem", "allowUntrustedIssuer", "expectedDocType", "at", "maxBytes", "maxDepth"], "mdoc.verifyIssuerSigned");
132
133
  if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
133
134
  throw new MdocError("mdoc/algorithms-required", "mdoc.verifyIssuerSigned: opts.algorithms is required");
134
135
  }
136
+ // Issuer authentication is the WHOLE point of verifyIssuerSigned. The signer
137
+ // cert rides in the attacker-supplied x5chain, so verifying the COSE_Sign1
138
+ // against that embedded key only proves the MSO is self-consistent — a forged
139
+ // mDL (attacker keypair, self-signed leaf, self-signed MSO) passes unless the
140
+ // chain is anchored to a configured trust root. Require trustAnchorsPem by
141
+ // default; an operator who genuinely wants trust-anchor-free verification must
142
+ // opt in EXPLICITLY (and gets issuerTrusted:false on the result so the
143
+ // unauthenticated posture is visible). Fail closed — never silently accept an
144
+ // unanchored issuer.
145
+ var hasAnchors = opts.trustAnchorsPem !== undefined && opts.trustAnchorsPem !== null;
146
+ if (!hasAnchors && opts.allowUntrustedIssuer !== true) {
147
+ throw new MdocError("mdoc/trust-anchors-required",
148
+ "mdoc.verifyIssuerSigned: opts.trustAnchorsPem is required to authenticate the issuer " +
149
+ "(the x5chain signer cert is attacker-controlled); pass trustAnchorsPem, or set " +
150
+ "allowUntrustedIssuer:true to explicitly accept an unauthenticated issuer");
151
+ }
135
152
  validateOpts.optionalDate(opts.at, "mdoc.verifyIssuerSigned: opts.at", MdocError, "mdoc/bad-at");
136
153
  var at = (opts.at !== undefined && opts.at !== null) ? opts.at : new Date();
137
154
  var decodeOpts = { allowedTags: ALLOWED_TAGS, maxBytes: opts.maxBytes, maxDepth: opts.maxDepth };
@@ -264,6 +281,10 @@ async function verifyIssuerSigned(issuerSigned, opts) {
264
281
  deviceKey: deviceKey,
265
282
  signerCert: signerCert.toString(),
266
283
  alg: verified.alg,
284
+ // true only when the signer chain was validated against a configured trust
285
+ // anchor; false on the explicit allowUntrustedIssuer opt-out so a caller
286
+ // can tell an authenticated issuer from a merely self-consistent MSO.
287
+ issuerTrusted: hasAnchors,
267
288
  };
268
289
  }
269
290
 
@@ -409,8 +430,10 @@ function _verifyChain(chainDer, anchorsPem, at) {
409
430
  throw new MdocError("mdoc/chain-loop", "mdoc.verifyIssuerSigned: certificate chain did not terminate");
410
431
  }
411
432
  function _issued(issuer, subject) {
412
- try { return subject.checkIssued(issuer) && subject.verify(issuer.publicKey); }
413
- catch (_e) { return false; }
433
+ // Enforces basicConstraints cA:TRUE on the issuer alongside the
434
+ // checkIssued + signature linkage — an IACA/DS chain cannot accept a
435
+ // non-CA cert as an issuer (basicConstraints bypass, CVE-2002-0862 class).
436
+ return x509Chain.issuerValidlyIssued(issuer, subject);
414
437
  }
415
438
  function _assertValidAt(cert, atMs) {
416
439
  if (atMs < cert.validFromDate.getTime() || atMs > cert.validToDate.getTime()) {
package/lib/metrics.js CHANGED
@@ -39,7 +39,6 @@
39
39
 
40
40
  var C = require("./constants");
41
41
  var canonicalJson = require("./canonical-json");
42
- var nodeFs = require("node:fs");
43
42
  var atomicFile = require("./atomic-file");
44
43
  var safeJson = require("./safe-json");
45
44
  var { defineClass } = require("./framework-error");
@@ -1067,8 +1066,20 @@ function snapshotRead(p) {
1067
1066
  MetricsError, "metrics-snapshot/bad-path");
1068
1067
  var raw;
1069
1068
  try {
1070
- raw = nodeFs.readFileSync(p, "utf8");
1069
+ // Capped fd-bound read: the snapshot is parsed AFTER read, so the cap must
1070
+ // precede the alloc — a hostile multi-GB file at the snapshot path would
1071
+ // otherwise OOM the reader before safeJson's 4 MiB parse cap is consulted.
1072
+ raw = atomicFile.fdSafeReadSync(p, {
1073
+ maxBytes: C.BYTES.mib(4), encoding: "utf8", refuseSymlink: true,
1074
+ errorFor: function (kind, detail) {
1075
+ if (kind === "enoent") return new MetricsError("metrics-snapshot/not-found", "metrics.snapshot.read: " + p + " — not found");
1076
+ if (kind === "too-large") return new MetricsError("metrics-snapshot/too-large", "metrics.snapshot.read: " + p + " exceeds " + detail.max + " bytes");
1077
+ if (kind === "symlink") return new MetricsError("metrics-snapshot/symlink-refused", "metrics.snapshot.read: " + p + " is a symlink (refused)");
1078
+ return undefined;
1079
+ },
1080
+ });
1071
1081
  } catch (e) {
1082
+ if (e instanceof MetricsError) throw e;
1072
1083
  throw new MetricsError("metrics-snapshot/not-found",
1073
1084
  "metrics.snapshot.read: " + p + " — " + (e && e.message ? e.message : String(e)));
1074
1085
  }
@@ -1329,7 +1340,7 @@ function shadowRegistry(opts) {
1329
1340
  var gaugeSet = _shadowSetOf(opts.gauges, "gauges");
1330
1341
  var infoSet = _shadowSetOf(opts.info, "info");
1331
1342
  var cap = opts.cardinalityCap === undefined ? SHADOW_DEFAULT_CARDINALITY : opts.cardinalityCap;
1332
- if (typeof cap !== "number" || !isFinite(cap) || cap < 1 || Math.floor(cap) !== cap) {
1343
+ if (!numericBounds.isPositiveFiniteInt(cap)) {
1333
1344
  throw new MetricsError("metrics-shadow/bad-cap",
1334
1345
  "shadowRegistry: cardinalityCap must be a positive integer");
1335
1346
  }
@@ -556,7 +556,7 @@ function create(opts) {
556
556
  _emitFailure(req, "shape");
557
557
  return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
558
558
  }
559
- if (typeof ctr !== "number" || !isFinite(ctr) || ctr < 0 || Math.floor(ctr) !== ctr) {
559
+ if (!numericBounds.isNonNegativeFiniteInt(ctr)) {
560
560
  _emitFailure(req, "shape");
561
561
  return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
562
562
  }
@@ -596,7 +596,7 @@ function create(opts) {
596
596
  _emitFailure(req, "shape");
597
597
  return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
598
598
  }
599
- if (!isFinite(ctr) || ctr < 0 || Math.floor(ctr) !== ctr) {
599
+ if (!numericBounds.isNonNegativeFiniteInt(ctr)) {
600
600
  _emitFailure(req, "shape");
601
601
  return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
602
602
  }
@@ -269,13 +269,19 @@ function _contentType(req) {
269
269
  }
270
270
 
271
271
  function _typeMatches(actual, allowed) {
272
+ var ab = actual.split("/");
272
273
  for (var i = 0; i < allowed.length; i++) {
273
274
  var a = allowed[i].toLowerCase();
274
- // Match either exact type or "type/*" prefix
275
+ // Exact match, or component-wise wildcard where `*` in either the type or
276
+ // subtype segment matches any value — so "*/*" (the raw() default) matches
277
+ // every type, "type/*" matches a whole type, and "*/json" matches a
278
+ // subtype across types. (A literal indexOf on "*/" never matched a real
279
+ // Content-Type, which made the "*/*" default reject every request.)
275
280
  if (a === actual) return true;
276
- var slash = a.indexOf("/");
277
- if (slash !== -1 && a.slice(slash + 1) === "*" &&
278
- actual.indexOf(a.slice(0, slash + 1)) === 0) return true;
281
+ var pb = a.split("/");
282
+ if (pb.length === 2 && ab.length === 2 &&
283
+ (pb[0] === "*" || pb[0] === ab[0]) &&
284
+ (pb[1] === "*" || pb[1] === ab[1])) return true;
279
285
  }
280
286
  return false;
281
287
  }
@@ -71,17 +71,6 @@ function _coerceAgentPattern(r, where) {
71
71
  "in operator code)");
72
72
  }
73
73
 
74
- // Bot-guard's "trust the proxy header" semantics for actor.ip — the
75
- // audit event records the apparent source even when behind a CDN, but
76
- // only when the operator opts in to trustProxy. Without the opt, we
77
- // stick to socket.remoteAddress so an attacker-forged XFF can't
78
- // pollute audit attribution.
79
- function _xffIpFor(trustProxy) {
80
- return function (req) {
81
- return requestHelpers.clientIp(req, { trustProxy: trustProxy });
82
- };
83
- }
84
-
85
74
  /**
86
75
  * @primitive b.middleware.botGuard
87
76
  * @signature b.middleware.botGuard(req, res, next)
@@ -115,7 +104,10 @@ function _xffIpFor(trustProxy) {
115
104
  * bodyOnBlock: string,
116
105
  * onDeny: function(req, res, info): void, // own the block response; info = { status, reason }
117
106
  * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
118
- * trustProxy: boolean|number,
107
+ * trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-For / -Proto
108
+ * clientIpResolver: function(req): string|null, // own the audit-actor IP
109
+ * protocolResolver: function(req): "http"|"https", // own the secure-context decision
110
+ * trustProxy: boolean|number, // legacy; refused unless paired with trustedProxies/resolver (spoofable)
119
111
  * }
120
112
  *
121
113
  * @example
@@ -131,11 +123,26 @@ function create(opts) {
131
123
  opts = opts || {};
132
124
  validateOpts(opts, [
133
125
  "mode", "onlyForHtml", "allowedAgents", "blockedAgents",
134
- "skipPaths", "statusOnBlock", "bodyOnBlock", "onDeny", "problemDetails", "trustProxy",
126
+ "skipPaths", "statusOnBlock", "bodyOnBlock", "onDeny", "problemDetails",
127
+ "trustProxy", "trustedProxies", "clientIpResolver", "protocolResolver",
135
128
  ], "middleware.botGuard");
136
- var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
137
- ? opts.trustProxy : false;
138
- var _xffIp = _xffIpFor(trustProxy);
129
+ // The single trustProxy opt drives two forwarded-header reads: the audit
130
+ // actor.ip (X-Forwarded-For) and the secure-context check (X-Forwarded-Proto,
131
+ // see _isSecureContext). Both are peer-gated — declare your reverse proxies
132
+ // via trustedProxies (CIDRs), or own resolution via clientIpResolver /
133
+ // protocolResolver. A bare trustProxy is refused: it would trust forgeable
134
+ // headers from any caller.
135
+ var _ipResolver, _proto;
136
+ try {
137
+ _ipResolver = requestHelpers.trustedClientIp({ trustedProxies: opts.trustedProxies, clientIpResolver: opts.clientIpResolver });
138
+ _proto = requestHelpers.trustedProtocol({ trustedProxies: opts.trustedProxies, protocolResolver: opts.protocolResolver });
139
+ } catch (e) { throw new BotGuardError("bot-guard/bad-opt", e.message); }
140
+ if ((opts.trustProxy === true || typeof opts.trustProxy === "number") && !_ipResolver.peerGated) {
141
+ throw new BotGuardError("bot-guard/bad-opt",
142
+ "trustProxy is spoofable — a direct caller could forge X-Forwarded-For / -Proto. Declare " +
143
+ "your reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] or supply clientIpResolver / protocolResolver.");
144
+ }
145
+ var _xffIp = _ipResolver.resolve;
139
146
  var mode = opts.mode || "block";
140
147
  var onlyForHtml = opts.onlyForHtml !== false;
141
148
  var allowedAgents = (opts.allowedAgents || []).map(function (r, i) {
@@ -163,9 +170,10 @@ function create(opts) {
163
170
  // app, a LAN / *.local reverse-proxy deployment — the browser omits
164
171
  // Sec-Fetch-* entirely, so a missing Sec-Fetch-Mode is NORMAL there and
165
172
  // must not be read as a bot signal. The effective scheme honours
166
- // X-Forwarded-Proto only under trustProxy (otherwise it is forgeable).
173
+ // X-Forwarded-Proto only from a trusted proxy peer (peer-gated), else the
174
+ // real TLS socket — a direct caller's forged header is ignored.
167
175
  function _isSecureContext(req) {
168
- if (requestHelpers.requestProtocol(req, { trustProxy: trustProxy }) === "https") return true;
176
+ if (_proto.resolve(req) === "https") return true;
169
177
  var host = (req.headers && req.headers.host) || "";
170
178
  host = String(host).toLowerCase().replace(/:\d+$/, ""); // strip :port
171
179
  if (host.charAt(0) === "[") { // [::1] IPv6 literal
@@ -113,7 +113,11 @@ function headerValue(types, label) {
113
113
  }
114
114
  for (var i = 0; i < types.length; i += 1) {
115
115
  var t = types[i];
116
- if (typeof t !== "string" || !KNOWN_TYPES[t]) {
116
+ // hasOwnProperty, not `KNOWN_TYPES[t]`: a bracket lookup on the plain-object
117
+ // allowlist resolves inherited members ("toString" / "constructor" /
118
+ // "hasOwnProperty") to truthy functions, so those would pass validation and
119
+ // be emitted as bogus Clear-Site-Data directives (prototype shadowing).
120
+ if (typeof t !== "string" || !Object.prototype.hasOwnProperty.call(KNOWN_TYPES, t)) {
117
121
  throw new TypeError(
118
122
  label + ": unknown type '" + t +
119
123
  "' (expected one of: " + Object.keys(KNOWN_TYPES).join(", ") + ")");
@@ -287,6 +287,28 @@ function composePipeline(entries, opts) {
287
287
  catch (finalErr) { return reject(finalErr); }
288
288
  resolve();
289
289
  }
290
+ // A middleware (or error handler) that ENDS THE RESPONSE without calling
291
+ // next has halted the chain: it handled the request itself. Settle the
292
+ // outer promise so the awaiting router is released — a never-settled
293
+ // promise pins its req/res closure forever — but do NOT call finalNext:
294
+ // the router's next-flag stays false, so it won't run the route handler
295
+ // on top of an already-sent response.
296
+ function _resolveOnce() {
297
+ if (finished) return;
298
+ finished = true;
299
+ resolve();
300
+ }
301
+ // Settle when the response actually finishes. This is response-driven,
302
+ // not return-driven: a callback-style middleware that calls next() LATER
303
+ // (from a timer, stream, or legacy callback) returns before next() runs,
304
+ // so we must NOT treat a bare return as a halt — only an ended response.
305
+ // The synchronous _responseEnded() check below covers a middleware that
306
+ // ended the response inline (and a mock res without an event emitter);
307
+ // this listener covers one that ends it from a deferred callback.
308
+ if (res && typeof res.once === "function") {
309
+ res.once("finish", _resolveOnce);
310
+ res.once("close", _resolveOnce);
311
+ }
290
312
  async function dispatch(err) {
291
313
  if (finished) return;
292
314
  if (idx >= resolved.length) return _finishOnce(err);
@@ -313,14 +335,19 @@ function composePipeline(entries, opts) {
313
335
  }
314
336
  try {
315
337
  if (err) {
316
- // Error handler: (err, req, res, next)
338
+ // Error handler: (err, req, res, next). Express convention — a
339
+ // 4-arg handler that returns without calling next has HANDLED the
340
+ // error, so the chain ends cleanly; settle (without finalNext).
317
341
  await entry.mw(err, req, res, _next);
318
- if (!advanced) _finishOnce();
342
+ if (!advanced) _resolveOnce();
319
343
  } else {
320
- // Regular middleware: (req, res, next)
344
+ // Regular middleware: (req, res, next). Settle only if it ENDED the
345
+ // response (a halt). A bare return without next is NOT treated as a
346
+ // halt — it may be a callback-style middleware that calls next()
347
+ // later (timer/stream); the finish/close listener covers a deferred
348
+ // response end, and a deferred next() continues the chain.
321
349
  await entry.mw(req, res, _next);
322
- // 3-arg middleware that doesn't call next — chain halts.
323
- // The middleware presumably wrote the response itself.
350
+ if (!advanced && _responseEnded(res)) _resolveOnce();
324
351
  }
325
352
  } catch (syncErr) {
326
353
  // Synchronous throw OR rejected promise — route through
@@ -334,6 +361,13 @@ function composePipeline(entries, opts) {
334
361
  };
335
362
  }
336
363
 
364
+ // True once the response has been committed/ended — the reliable "this
365
+ // middleware handled the request" signal (vs. the function merely returning,
366
+ // which a callback-style middleware does before its deferred next()).
367
+ function _responseEnded(res) {
368
+ return !!(res && (res.writableEnded || res.finished || res.headersSent));
369
+ }
370
+
337
371
  composePipeline.CANONICAL_POSITIONS = CANONICAL_POSITIONS;
338
372
  composePipeline.ComposePipelineError = ComposePipelineError;
339
373
 
@@ -328,6 +328,15 @@ function create(opts) {
328
328
  // Status precludes body? skip.
329
329
  if (NO_BODY_STATUS.has(statusCode)) { compress = false; return; }
330
330
 
331
+ // 206 Partial Content / any Content-Range response must pass through
332
+ // UNtransformed (RFC 7233 §4.1) — compressing it drops Content-Length but
333
+ // leaves Content-Range advertising an uncompressed byte interval over a
334
+ // now-compressed body, corrupting range-assembling clients.
335
+ if (statusCode === 206) { compress = false; return; }
336
+ var crRange = (headersObj && headersObj["content-range"]) ||
337
+ (originalGetHeader && originalGetHeader("Content-Range"));
338
+ if (crRange) { compress = false; return; }
339
+
331
340
  // Already compressed by the handler / earlier middleware? skip.
332
341
  var existingCE = (headersObj && headersObj["content-encoding"]) ||
333
342
  (originalGetHeader && originalGetHeader("Content-Encoding"));
@@ -58,15 +58,6 @@ var validateOpts = require("../validate-opts");
58
58
  var denyResponse = require("./deny-response").denyResponse;
59
59
  var { defineClass } = require("../framework-error");
60
60
 
61
- // CORS audit events use the proxy-aware client IP only when the
62
- // operator opts in via `trustProxy`. Default refuses forwarded
63
- // headers — same boundary as the rest of the v0.5.3 trustProxy sweep.
64
- function _xffIpFor(trustProxy) {
65
- return function (req) {
66
- return requestHelpers.clientIp(req, { trustProxy: trustProxy });
67
- };
68
- }
69
-
70
61
  var CorsError = defineClass("CorsError", { alwaysPermanent: true });
71
62
 
72
63
  // allowList entries:
@@ -117,17 +108,18 @@ function _canonicalOrigin(input) {
117
108
  // supplied. Works for direct deployments (no proxy); operators behind
118
109
  // a TLS-terminating proxy that doesn't forward correct Host should set
119
110
  // opts.siteOrigin explicitly.
120
- function _inferRequestOrigin(req, trustProxy) {
111
+ function _inferRequestOrigin(req, protoResolve) {
121
112
  if (!req || !req.headers) return null;
122
113
  var host = req.headers.host;
123
114
  if (!host) return null;
124
- // Protocol resolution honors the operator's trustProxy opt without
125
- // it, X-Forwarded-Proto is ignored as attacker-forgeable.
126
- var proto = requestHelpers.requestProtocol(req, { trustProxy: trustProxy });
115
+ // Peer-gated protocol resolution X-Forwarded-Proto is honored only from a
116
+ // trusted proxy (else the TLS socket), so a direct caller can't forge the
117
+ // inferred origin's scheme to slip past the same-origin check.
118
+ var proto = protoResolve(req);
127
119
  return _canonicalOrigin(proto + "://" + host);
128
120
  }
129
121
 
130
- function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy, strictNullOrigin) {
122
+ function _isSameOrigin(req, originHeader, configuredSiteOrigins, protoResolve, strictNullOrigin) {
131
123
  // Origin: null arrives when a browser opaques the Origin (e.g.
132
124
  // Referrer-Policy: no-referrer on the page). Sec-Fetch-Site can
133
125
  // distinguish the same-origin case, but non-browser clients can forge
@@ -150,10 +142,10 @@ function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy, str
150
142
  }
151
143
  return false;
152
144
  }
153
- // Fall back to inferring from the request itself. trustProxy threads
154
- // through so operators behind a TLS terminator with X-Forwarded-Proto
155
- // can opt in to consult the header.
156
- var reqOrigin = _inferRequestOrigin(req, trustProxy);
145
+ // Fall back to inferring from the request itself. The peer-gated protocol
146
+ // resolver threads through so operators behind a TLS terminator consult
147
+ // X-Forwarded-Proto only from a trusted peer.
148
+ var reqOrigin = _inferRequestOrigin(req, protoResolve);
157
149
  return reqOrigin !== null && reqOrigin === canonOrigin;
158
150
  }
159
151
 
@@ -185,7 +177,10 @@ function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy, str
185
177
  * maxAgeSeconds: number, // default 600
186
178
  * refuseUnknown: boolean, // default true
187
179
  * strictNullOrigin: boolean, // default true
188
- * trustProxy: boolean|number,
180
+ * trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-Proto for same-origin inference
181
+ * protocolResolver: function(req): "http"|"https", // own the HTTPS decision
182
+ * clientIpResolver: function(req): string|null, // own the audit-actor IP
183
+ * trustProxy: boolean|number, // legacy; refused unless paired with trustedProxies/resolver (spoofable)
189
184
  * onDeny: function(req, res, info): void, // own every refusal; info = { status, reason, origin, header? }
190
185
  * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
191
186
  * }
@@ -205,11 +200,25 @@ function create(opts) {
205
200
  validateOpts(opts, [
206
201
  "origins", "siteOrigin", "methods", "headers", "exposeHeaders",
207
202
  "credentials", "maxAgeSeconds", "refuseUnknown", "trustProxy",
203
+ "trustedProxies", "clientIpResolver", "protocolResolver",
208
204
  "strictNullOrigin", "allowPrivateNetwork", "onDeny", "problemDetails",
209
205
  ], "middleware.cors");
210
- var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
211
- ? opts.trustProxy : false;
212
- var _xffIp = _xffIpFor(trustProxy);
206
+ // The request scheme feeds the same-origin determination, and a bare
207
+ // trustProxy trusts a forgeable X-Forwarded-Proto from any caller. Peer-gate
208
+ // both protocol and the audit-actor IP via trustedProxies (CIDRs) or own them
209
+ // via protocolResolver / clientIpResolver. A bare trustProxy is refused.
210
+ var _proto, _ip;
211
+ try {
212
+ _proto = requestHelpers.trustedProtocol({ trustedProxies: opts.trustedProxies, protocolResolver: opts.protocolResolver });
213
+ _ip = requestHelpers.trustedClientIp({ trustedProxies: opts.trustedProxies, clientIpResolver: opts.clientIpResolver });
214
+ } catch (e) { throw new CorsError("cors/bad-opt", e.message); }
215
+ if ((opts.trustProxy === true || typeof opts.trustProxy === "number") && !_proto.peerGated) {
216
+ throw new CorsError("cors/bad-opt",
217
+ "trustProxy is spoofable — a direct caller could forge X-Forwarded-Proto to alter the " +
218
+ "same-origin decision. Declare your reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] " +
219
+ "or supply protocolResolver(req) / clientIpResolver(req).");
220
+ }
221
+ var _xffIp = _ip.resolve;
213
222
 
214
223
  // Build a canonicalized allowList at create() time. String entries
215
224
  // get parsed through _canonicalOrigin so case + default-port
@@ -296,7 +305,7 @@ function create(opts) {
296
305
  // Same-origin POST/PUT/etc. carry an Origin header per the Fetch
297
306
  // spec but should not be subject to CORS allow-listing — they're
298
307
  // the operator's own site talking to itself.
299
- if (_isSameOrigin(req, origin, siteOrigins, trustProxy, strictNullOrigin)) return next();
308
+ if (_isSameOrigin(req, origin, siteOrigins, _proto.resolve, strictNullOrigin)) return next();
300
309
 
301
310
  var matched = _matchOrigin(origin, origins);
302
311
  if (!matched) {