@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
@@ -115,6 +115,8 @@
115
115
  */
116
116
 
117
117
  var nodeFs = require("node:fs");
118
+ var pick = require("../pick");
119
+ var numericBounds = require("../numeric-bounds");
118
120
  var os = require("node:os");
119
121
  var nodePath = require("node:path");
120
122
  var nodeCrypto = require("node:crypto");
@@ -164,7 +166,6 @@ var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
164
166
  // Mirrors safe-json.js + safe-schema.js. Field names that match these
165
167
  // are refused at the parse boundary regardless of which sub-parser is
166
168
  // in play — consistent prototype-pollution defense across the framework.
167
- var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
168
169
 
169
170
  // Materialize a header/parameter map from request-derived [key, value]
170
171
  // pairs WITHOUT a computed member write (`target[key] = value`). A
@@ -183,7 +184,7 @@ var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
183
184
  function _mapFromPairs(pairs) {
184
185
  var safe = [];
185
186
  for (var i = 0; i < pairs.length; i++) {
186
- if (POISONED_KEYS.has(pairs[i][0])) continue;
187
+ if (pick.isPoisonedKey(pairs[i][0])) continue;
187
188
  safe.push(pairs[i]);
188
189
  }
189
190
  return Object.assign(Object.create(null), Object.fromEntries(safe));
@@ -256,29 +257,31 @@ function _contentType(req) {
256
257
  // would slice through quoted commas/semicolons and corrupt the
257
258
  // multipart boundary. Use the shared quote-aware splitter that
258
259
  // tracks RFC 8941 §3.3.3 quoted-string state with backslash-escape.
259
- var parts = structuredFields.splitTopLevel(rest, ";");
260
- for (var i = 0; i < parts.length; i++) {
261
- var p = parts[i].trim();
262
- var eq = p.indexOf("=");
263
- if (eq === -1) continue;
264
- var k = p.slice(0, eq).trim().toLowerCase();
265
- var v = p.slice(eq + 1).trim();
260
+ var kvps = structuredFields.parseKeyValuePieces(
261
+ structuredFields.splitTopLevel(rest, ";"));
262
+ structuredFields.forEachKeyValue(kvps, function (key, v) {
266
263
  var _unq = structuredFields.unquoteSfString(v);
267
264
  if (_unq !== null) v = _unq;
268
- paramPairs.push([k, v]);
269
- }
265
+ paramPairs.push([key, v]);
266
+ });
270
267
  }
271
268
  return { type: type, params: _mapFromPairs(paramPairs) };
272
269
  }
273
270
 
274
271
  function _typeMatches(actual, allowed) {
272
+ var ab = actual.split("/");
275
273
  for (var i = 0; i < allowed.length; i++) {
276
274
  var a = allowed[i].toLowerCase();
277
- // 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.)
278
280
  if (a === actual) return true;
279
- var slash = a.indexOf("/");
280
- if (slash !== -1 && a.slice(slash + 1) === "*" &&
281
- 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;
282
285
  }
283
286
  return false;
284
287
  }
@@ -430,26 +433,17 @@ function _bufferBody(req, limit) {
430
433
  return;
431
434
  }
432
435
  }
433
- var collector = safeBuffer.boundedChunkCollector({
436
+ safeBuffer.collectStream(req, {
434
437
  maxBytes: limit,
435
438
  errorClass: BodyParserError,
436
439
  sizeCode: "body-parser/too-large",
437
440
  sizeMessage: "request body exceeds limit",
441
+ }).then(resolve, function (e) {
442
+ // The drain-overflow guard (a second line of defense behind the
443
+ // Content-Length pre-check above) carries the 413 status code.
444
+ if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
445
+ reject(e);
438
446
  });
439
- var done = false;
440
- req.on("data", function (chunk) {
441
- if (done) return;
442
- try { collector.push(chunk); }
443
- catch (e) {
444
- done = true;
445
- try { req.destroy(); } catch (_e) { /* socket already closed */ }
446
- if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
447
- reject(e);
448
- return;
449
- }
450
- });
451
- req.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
452
- req.on("error", function (e) { if (!done) { done = true; reject(e); } });
453
447
  });
454
448
  }
455
449
 
@@ -520,7 +514,7 @@ async function _parseUrlencoded(req, opts) {
520
514
  for (var i = 0; i < keys.length; i++) {
521
515
  var k = keys[i][0];
522
516
  var v = keys[i][1];
523
- if (POISONED_KEYS.has(k)) {
517
+ if (pick.isPoisonedKey(k)) {
524
518
  throw new BodyParserError(
525
519
  "body-parser/urlencoded-poisoned-key",
526
520
  "urlencoded body contains forbidden key '" + k + "' (prototype-pollution defense)",
@@ -644,10 +638,10 @@ function _parseMultipartHeaders(rawHeaders) {
644
638
  true, HTTP_STATUS.BAD_REQUEST
645
639
  );
646
640
  }
647
- var idx = line.indexOf(":");
648
- if (idx === -1) continue;
649
- var k = line.slice(0, idx).trim().toLowerCase();
650
- var v = line.slice(idx + 1).trim();
641
+ var khv = structuredFields.parseKeyValuePiece(line, ":");
642
+ if (khv.value === null) continue;
643
+ var k = khv.key;
644
+ var v = khv.value.trim();
651
645
  for (var j = 0; j < v.length; j++) {
652
646
  var c = v.charCodeAt(j);
653
647
  if (c === 0 || c === 10 || c === 13) { // NUL/LF/CR forbidden in field-value (RFC 9110 §5.5)
@@ -736,12 +730,8 @@ function _parseHeaderParams(headerValue, filenameCharsets) {
736
730
  // it last so Object.fromEntries' last-wins resolves it.
737
731
  var paramPairs = [["_value", parts[0].trim().toLowerCase()]];
738
732
  var extName = null;
739
- for (var i = 1; i < parts.length; i++) {
740
- var p = parts[i].trim();
741
- var eq = p.indexOf("=");
742
- if (eq === -1) continue;
743
- var k = p.slice(0, eq).trim().toLowerCase();
744
- var v = p.slice(eq + 1).trim();
733
+ var kvps = structuredFields.parseKeyValuePieces(parts, 1);
734
+ structuredFields.forEachKeyValue(kvps, function (k, v) {
745
735
  var _unq = structuredFields.unquoteSfString(v);
746
736
  if (_unq !== null) v = _unq;
747
737
  if (k.charAt(k.length - 1) === "*") {
@@ -751,10 +741,10 @@ function _parseHeaderParams(headerValue, filenameCharsets) {
751
741
  if (bareKey === "filename") extName = decoded;
752
742
  paramPairs.push([bareKey, decoded]);
753
743
  }
754
- continue;
744
+ return;
755
745
  }
756
746
  paramPairs.push([k, v]);
757
- }
747
+ });
758
748
  if (extName !== null) paramPairs.push(["filename", extName]);
759
749
  return _mapFromPairs(paramPairs);
760
750
  }
@@ -952,7 +942,7 @@ async function _parseMultipart(req, opts, ctParams) {
952
942
  // Read until \r\n\r\n.
953
943
  var headEnd = pending.indexOf("\r\n\r\n");
954
944
  if (headEnd === -1) {
955
- if (pending.length > C.BYTES.kib(16)) {
945
+ if (safeBuffer.byteLengthOf(pending) > C.BYTES.kib(16)) {
956
946
  done(new BodyParserError("body-parser/multipart-headers-too-large",
957
947
  "multipart part headers exceed 16KB", true, 413));
958
948
  return;
@@ -986,7 +976,7 @@ async function _parseMultipart(req, opts, ctParams) {
986
976
  "multipart part missing form-data Content-Disposition", true, HTTP_STATUS.BAD_REQUEST));
987
977
  return;
988
978
  }
989
- if (POISONED_KEYS.has(cd.name)) {
979
+ if (pick.isPoisonedKey(cd.name)) {
990
980
  done(new BodyParserError("body-parser/multipart-poisoned-field",
991
981
  "multipart field '" + cd.name + "' is forbidden (prototype-pollution defense)",
992
982
  true, HTTP_STATUS.BAD_REQUEST));
@@ -1654,12 +1644,9 @@ function _resolveStandaloneMultipartOpts(opts, ct) {
1654
1644
  }
1655
1645
  if (opts.maxFiles !== undefined) {
1656
1646
  var mf = opts.maxFiles;
1657
- var mfBad = typeof mf !== "number" || !isFinite(mf) || mf <= 0 || Math.floor(mf) !== mf;
1658
- if (mfBad) {
1659
- throw new BodyParserError("body-parser/bad-max-files",
1660
- "parsers.multipart: opts.maxFiles must be a positive integer",
1661
- true, HTTP_STATUS.BAD_REQUEST);
1662
- }
1647
+ numericBounds.requirePositiveFiniteInt(mf,
1648
+ "parsers.multipart: opts.maxFiles", BodyParserError, "body-parser/bad-max-files",
1649
+ null, { permanent: true, statusCode: HTTP_STATUS.BAD_REQUEST });
1663
1650
  resolved.fileCount = mf;
1664
1651
  }
1665
1652
  // Pass-through overrides for the multipart-specific knobs the middleware
@@ -1705,5 +1692,7 @@ module.exports = {
1705
1692
  _contentType: _contentType,
1706
1693
  _hasBody: _hasBody,
1707
1694
  _sanitizeFilename: _sanitizeFilename,
1708
- POISONED_KEYS: POISONED_KEYS,
1695
+ // Sourced from the canonical pick primitive (no hand-rolled set); a fresh
1696
+ // Set so the long-standing `instanceof Set` shape of this export is kept.
1697
+ POISONED_KEYS: new Set(pick.POISONED_KEYS),
1709
1698
  };
@@ -30,6 +30,7 @@ var defineClass = require("../framework-error").defineClass;
30
30
  var lazyRequire = require("../lazy-require");
31
31
  var validateOpts = require("../validate-opts");
32
32
  var safeBuffer = require("../safe-buffer");
33
+ var requestHelpers = require("../request-helpers");
33
34
 
34
35
  var audit = lazyRequire(function () { return require("../audit"); });
35
36
 
@@ -93,17 +94,13 @@ function create(opts) {
93
94
  var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
94
95
  ? opts.auditAction : "middleware.bot_disclose";
95
96
 
97
+ // null mountPaths = apply to every route; otherwise reuse the shared
98
+ // segment-boundary matcher (built once).
99
+ var _mountMatch = mountPaths
100
+ ? requestHelpers.makeSkipMatcher({ skipPaths: mountPaths }, "middleware.botDisclose")
101
+ : null;
96
102
  function _matches(req) {
97
- if (!mountPaths) return true; // null = match every path
98
- var p = req.url || "";
99
- var qpos = p.indexOf("?");
100
- if (qpos !== -1) p = p.slice(0, qpos);
101
- for (var i = 0; i < mountPaths.length; i++) {
102
- var m = mountPaths[i];
103
- if (typeof m === "string" && (p === m || p.indexOf(m + "/") === 0)) return true;
104
- if (m instanceof RegExp && m.test(p)) return true;
105
- }
106
- return false;
103
+ return _mountMatch ? _mountMatch(req) : true;
107
104
  }
108
105
 
109
106
  function _emitAudit(action, outcome, metadata) {
@@ -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) {
@@ -144,21 +151,13 @@ function create(opts) {
144
151
  var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
145
152
  return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
146
153
  }));
147
- var skipPaths = opts.skipPaths || [];
148
154
  var statusOnBlock = opts.statusOnBlock || 403;
149
155
  var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
150
156
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
151
157
  var problemMode = opts.problemDetails === true;
152
158
 
153
- function _shouldSkip(req) {
154
- var path = req.pathname || req.url || "/";
155
- for (var i = 0; i < skipPaths.length; i++) {
156
- if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
157
- return true;
158
- }
159
- }
160
- return false;
161
- }
159
+ // Path-exemption predicate (string-prefix or RegExp), validated at create().
160
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.botGuard");
162
161
 
163
162
  function _looksLikeApi(req) {
164
163
  var path = req.pathname || req.url || "/";
@@ -171,9 +170,10 @@ function create(opts) {
171
170
  // app, a LAN / *.local reverse-proxy deployment — the browser omits
172
171
  // Sec-Fetch-* entirely, so a missing Sec-Fetch-Mode is NORMAL there and
173
172
  // must not be read as a bot signal. The effective scheme honours
174
- // 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.
175
175
  function _isSecureContext(req) {
176
- if (requestHelpers.requestProtocol(req, { trustProxy: trustProxy }) === "https") return true;
176
+ if (_proto.resolve(req) === "https") return true;
177
177
  var host = (req.headers && req.headers.host) || "";
178
178
  host = String(host).toLowerCase().replace(/:\d+$/, ""); // strip :port
179
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(", ") + ")");
@@ -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) {
@@ -67,6 +67,7 @@
67
67
  * }
68
68
  */
69
69
  var lazyRequire = require("../lazy-require");
70
+ var pick = require("../pick");
70
71
  var forms = require("../forms");
71
72
  var requestHelpers = require("../request-helpers");
72
73
  var validateOpts = require("../validate-opts");
@@ -76,6 +77,9 @@ var audit = lazyRequire(function () { return require("../audit"); });
76
77
  var DEFAULT_FIELD_NAME = "_csrf";
77
78
  var DEFAULT_HEADER_NAME = "X-CSRF-Token";
78
79
  var DEFAULT_METHODS = Object.freeze(["POST", "PUT", "DELETE", "PATCH"]);
80
+ // Per-process counter giving each csrfProtect mount a unique idempotency id so
81
+ // a stricter sub-route instance is not silently disabled by an earlier one.
82
+ var _csrfGateSeq = 0;
79
83
 
80
84
  // Default cookie name uses the RFC 6265bis __Host- prefix when the request
81
85
  // is over HTTPS. The prefix forces browsers to refuse the cookie unless
@@ -111,7 +115,7 @@ function _parseCookieHeader(header) {
111
115
  if (eq === -1) continue;
112
116
  var k = p.slice(0, eq).trim();
113
117
  if (k.length === 0) continue;
114
- if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
118
+ if (pick.isPoisonedKey(k)) continue;
115
119
  if (seen.has(k)) continue; // first-occurrence wins
116
120
  seen.add(k);
117
121
  var v = p.slice(eq + 1).trim();
@@ -123,17 +127,6 @@ function _parseCookieHeader(header) {
123
127
  return Object.assign(Object.create(null), Object.fromEntries(pairs));
124
128
  }
125
129
 
126
- // `_isHttps` defers to `requestHelpers.requestProtocol` so the
127
- // per-middleware `trustProxy` opt gates whether X-Forwarded-Proto is
128
- // consulted. Without trustProxy, an attacker could otherwise forge
129
- // the header to force the Secure cookie attribute (and inversely,
130
- // suppress it) on direct-to-server connections.
131
- function _isHttpsFor(trustProxy) {
132
- return function (req) {
133
- return requestHelpers.requestProtocol(req, { trustProxy: trustProxy }) === "https";
134
- };
135
- }
136
-
137
130
  function _formatSetCookie(name, value, opts) {
138
131
  var parts = [name + "=" + value];
139
132
  parts.push("Path=" + (opts.path || "/"));
@@ -274,7 +267,9 @@ function _writeReject(req, res, message, reason, onDeny, problemMode) {
274
267
  * allowedOrigins: string[],
275
268
  * requireOrigin: boolean,
276
269
  * requireJsonContentType: boolean,
277
- * trustProxy: boolean|number,
270
+ * trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-Proto for the Secure-cookie decision
271
+ * protocolResolver: function(req): "http"|"https", // own the HTTPS decision
272
+ * trustProxy: boolean|number, // legacy; refused unless paired with trustedProxies/protocolResolver (spoofable)
278
273
  * audit: boolean,
279
274
  * skipStateless: boolean, // default false — skip validation for Authorization-header / cookieless (not-CSRF-able) requests
280
275
  * onDeny: function(req, res, info): void, // own the 403; info = { status, reason }
@@ -295,14 +290,31 @@ function create(opts) {
295
290
 
296
291
  validateOpts(opts, [
297
292
  "cookie", "tokenLookup", "fieldName", "headerName", "methods", "audit",
298
- "trustProxy", "checkOrigin", "allowedOrigins", "requireJsonContentType",
299
- "requireOrigin", "skipStateless", "onDeny", "problemDetails",
293
+ "trustProxy", "trustedProxies", "protocolResolver",
294
+ "checkOrigin", "allowedOrigins", "requireJsonContentType",
295
+ "requireOrigin", "skipStateless", "skipPaths", "skip", "onDeny", "problemDetails",
300
296
  ], "middleware.csrfProtect");
301
297
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
302
298
  var problemMode = opts.problemDetails === true;
303
- var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
304
- ? opts.trustProxy : false;
305
- var _isHttps = _isHttpsFor(trustProxy);
299
+ // The Secure-cookie decision turns on whether the request is HTTPS, which
300
+ // behind a proxy comes from X-Forwarded-Proto. A bare trustProxy trusts that
301
+ // forgeable header from any caller — a direct request could suppress the
302
+ // Secure flag (cookie downgrade) or force it. Peer-gate it: declare your
303
+ // reverse proxies via trustedProxies, or own the decision via
304
+ // protocolResolver. A bare trustProxy is refused at construction.
305
+ var _proto;
306
+ try {
307
+ _proto = requestHelpers.trustedProtocol({
308
+ trustedProxies: opts.trustedProxies,
309
+ protocolResolver: opts.protocolResolver,
310
+ });
311
+ } catch (e) { throw new Error("middleware.csrfProtect: " + e.message); }
312
+ if ((opts.trustProxy === true || typeof opts.trustProxy === "number") && !_proto.peerGated) {
313
+ throw new Error("middleware.csrfProtect: trustProxy is spoofable for the Secure-cookie " +
314
+ "decision — a direct caller could forge X-Forwarded-Proto. Declare your reverse proxies " +
315
+ "via trustedProxies: [\"10.0.0.0/8\", …] or supply protocolResolver(req).");
316
+ }
317
+ function _isHttps(req) { return _proto.resolve(req) === "https"; }
306
318
 
307
319
  // Throw at create() — exactly one issuance source allowed.
308
320
  var hasCookie = opts.cookie != null && opts.cookie !== false;
@@ -317,8 +329,22 @@ function create(opts) {
317
329
 
318
330
  var fieldName = opts.fieldName || DEFAULT_FIELD_NAME;
319
331
  var headerName = (opts.headerName || DEFAULT_HEADER_NAME).toLowerCase();
332
+ // An empty methods array is truthy → `opts.methods || DEFAULT_METHODS` keeps
333
+ // `[]` and the method-gate matches nothing, silently disabling the primary
334
+ // CSRF check for all state-changing requests. Reject at config time.
335
+ if (opts.methods !== undefined) {
336
+ if (!Array.isArray(opts.methods) || opts.methods.length === 0 ||
337
+ !opts.methods.every(function (m) { return typeof m === "string" && m.length > 0; })) {
338
+ throw new Error("middleware.csrfProtect: opts.methods must be a non-empty array of HTTP method tokens (omit it for the POST/PUT/DELETE/PATCH default)");
339
+ }
340
+ }
320
341
  var methods = (opts.methods || DEFAULT_METHODS).map(function (m) { return m.toUpperCase(); });
321
342
  var auditOn = opts.audit !== false;
343
+ // Per-instance idempotency id — a stricter sub-route csrf mount must not be
344
+ // silently disabled by an earlier lenient one sharing a global flag (token
345
+ // issue is idempotent and double-validate is safe, so per-instance re-run is
346
+ // sound).
347
+ var GATE_ID = "csrf:" + (_csrfGateSeq++);
322
348
 
323
349
  // Origin / Referer cross-check — second-line defense alongside the
324
350
  // double-submit token. If the request's Origin (or Referer when
@@ -368,6 +394,13 @@ function create(opts) {
368
394
  // request always carries a Cookie header and is validated.
369
395
  var skipStateless = opts.skipStateless === true;
370
396
 
397
+ // Per-path exemption (string-prefix / RegExp / skip predicate), validated at
398
+ // create(). Lets one route (an RFC 8058 List-Unsubscribe-Post endpoint, a
399
+ // webhook, a bearer-token API path on the same app) opt out of the
400
+ // double-submit check while the app-level mount stays in place — the token is
401
+ // still issued for any later browser flow.
402
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.csrfProtect");
403
+
371
404
  // Cookie issuance config (only when opts.cookie is set).
372
405
  var cookieCfg = null;
373
406
  if (hasCookie) {
@@ -444,6 +477,17 @@ function create(opts) {
444
477
  req.csrfToken = existing;
445
478
  return existing;
446
479
  }
480
+ // Cookie issuance is idempotent at the RESPONSE level, keyed by cookie
481
+ // name: a redundant mount (createApp wired csrf AND an operator re-mounted
482
+ // it with the same cookie name) must emit a single Set-Cookie, not one per
483
+ // instance. Enforcement stays per instance (each gate still validates the
484
+ // token below) — only the response-cookie resource is deduped, so a mount
485
+ // with a DIFFERENT cookie name still issues its own.
486
+ if (!req._csrfIssuedCookies) req._csrfIssuedCookies = Object.create(null);
487
+ if (Object.prototype.hasOwnProperty.call(req._csrfIssuedCookies, cookieName)) {
488
+ req.csrfToken = req._csrfIssuedCookies[cookieName];
489
+ return req.csrfToken;
490
+ }
447
491
  if (existing && !/^[a-f0-9]{64}$/.test(existing)) {
448
492
  // Audit-emit so operators see when a planted/short cookie is
449
493
  // refused — surfaces the attack class in compliance logs.
@@ -464,22 +508,28 @@ function create(opts) {
464
508
  maxAge: cookieCfg.maxAge,
465
509
  });
466
510
  _appendSetCookie(res, setCookie);
511
+ req._csrfIssuedCookies[cookieName] = fresh;
467
512
  req.csrfToken = fresh;
468
513
  return fresh;
469
514
  }
470
515
 
471
516
  return function csrfProtect(req, res, next) {
472
- // Idempotent: a second csrf mount this request (e.g. createApp wired
473
- // it AND an operator mounted it again) is a no-op the first instance
474
- // already issued + validated.
475
- if (req._csrfApplied) return next();
476
- req._csrfApplied = true;
517
+ // Idempotent PER INSTANCE: the SAME mount running twice (createApp wired it
518
+ // AND an operator mounted it again) is a no-op, but a distinct stricter
519
+ // sub-route instance still runs — a shared global flag let the first
520
+ // (lenient) mount disable the second.
521
+ if (!req._csrfGates) req._csrfGates = Object.create(null);
522
+ if (req._csrfGates[GATE_ID]) return next();
523
+ req._csrfGates[GATE_ID] = true;
477
524
 
478
525
  // Issue/refresh the token on EVERY request (safe + state-changing)
479
526
  // when running in cookie mode — templates rendered after a POST
480
527
  // (e.g. error response) still need req.csrfToken populated.
481
528
  var expected = _issueIfNeeded(req, res);
482
529
 
530
+ // Exempt routes skip the double-submit check (token still issued above).
531
+ if (_shouldSkip(req)) return next();
532
+
483
533
  if (methods.indexOf(req.method) === -1) return next();
484
534
 
485
535
  // Stateless / token-authenticated requests are not CSRF-able — the