@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.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +45 -29
- package/lib/acme.js +6 -5
- package/lib/agent-event-bus.js +18 -4
- package/lib/agent-idempotency.js +7 -6
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-snapshot.js +32 -2
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-capability.js +1 -6
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-dp.js +1 -5
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-input.js +2 -2
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +14 -24
- package/lib/archive-tar-read.js +56 -24
- package/lib/archive.js +6 -12
- package/lib/arg-parser.js +7 -6
- package/lib/asn1-der.js +70 -22
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +312 -33
- package/lib/audit-chain.js +183 -54
- package/lib/audit-daily-review.js +36 -16
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +258 -0
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +91 -2
- package/lib/auth/access-lock.js +7 -29
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +29 -36
- package/lib/auth/fido-mds3.js +14 -16
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +15 -17
- package/lib/auth/lockout.js +24 -27
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +4 -4
- package/lib/auth/password.js +8 -2
- package/lib/auth/saml.js +70 -40
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +25 -5
- package/lib/auth/status-list.js +21 -9
- package/lib/auth/step-up.js +15 -13
- package/lib/auth-bot-challenge.js +27 -39
- package/lib/backup/bundle.js +18 -20
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +55 -67
- package/lib/backup/manifest.js +7 -1
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/break-glass.js +41 -22
- package/lib/cache.js +7 -18
- package/lib/cbor.js +36 -12
- package/lib/cdn-cache-control.js +15 -12
- package/lib/cert.js +8 -13
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +10 -2
- package/lib/client-hints.js +7 -9
- package/lib/cloud-events.js +43 -33
- package/lib/cluster-storage.js +9 -41
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +84 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +2 -12
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +11 -21
- package/lib/compliance.js +3 -10
- package/lib/config-drift.js +24 -20
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +21 -12
- package/lib/cookies.js +15 -13
- package/lib/cose.js +39 -11
- package/lib/cra-report.js +1 -11
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +34 -33
- package/lib/crypto.js +138 -7
- package/lib/csp.js +239 -3
- package/lib/daemon.js +46 -42
- package/lib/data-act.js +46 -13
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +75 -49
- package/lib/db.js +66 -27
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +30 -20
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +74 -30
- package/lib/fda-21cfr11.js +42 -13
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +911 -39
- package/lib/gdpr-ropa.js +22 -22
- package/lib/graphql-federation.js +18 -5
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +3 -2
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +17 -14
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +2 -2
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +12 -60
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +74 -128
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +9 -10
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +101 -109
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +3 -2
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +6 -5
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +13 -14
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +9 -12
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +103 -149
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cache.js +5 -12
- package/lib/http-client-cookie-jar.js +35 -16
- package/lib/http-client.js +233 -74
- package/lib/http-message-signature.js +3 -2
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +12 -27
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +30 -23
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +4 -3
- package/lib/mail-auth.js +43 -69
- package/lib/mail-bimi.js +35 -55
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +15 -14
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +17 -30
- package/lib/mail-helo.js +4 -7
- package/lib/mail-journal.js +13 -9
- package/lib/mail-mdn.js +12 -7
- package/lib/mail-rbl.js +7 -12
- package/lib/mail-scan.js +5 -7
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +39 -52
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +98 -2
- package/lib/mail-spam-score.js +9 -12
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +28 -13
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +78 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +7 -5
- package/lib/mdoc.js +36 -14
- package/lib/metrics.js +46 -33
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +13 -24
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +41 -52
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +28 -28
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +73 -23
- package/lib/middleware/daily-byte-quota.js +12 -30
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +35 -4
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +45 -44
- package/lib/middleware/scim-server.js +5 -3
- package/lib/middleware/security-headers.js +33 -7
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +14 -29
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +125 -41
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +98 -7
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +18 -17
- package/lib/network-heartbeat.js +3 -2
- package/lib/network-smtp-policy.js +52 -46
- package/lib/network-tls.js +67 -47
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +2 -12
- package/lib/nist-crosswalk.js +1 -1
- package/lib/nonce-store.js +81 -3
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +31 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +6 -47
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +14 -75
- package/lib/object-store/http-put.js +2 -7
- package/lib/object-store/http-request.js +157 -0
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +10 -79
- package/lib/observability-otlp-exporter.js +29 -29
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +54 -41
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/parsers/safe-yaml.js +28 -9
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +9 -4
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +334 -29
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +24 -22
- package/lib/retention.js +23 -9
- package/lib/router.js +17 -4
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +14 -28
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +51 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +13 -27
- package/lib/sandbox-worker.js +15 -2
- package/lib/sandbox.js +4 -3
- package/lib/scheduler.js +22 -13
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +4 -11
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +219 -7
- package/lib/sql.js +228 -81
- package/lib/sse.js +6 -10
- package/lib/static.js +201 -111
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +275 -16
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +53 -38
- package/lib/tsa.js +16 -17
- package/lib/validate-opts.js +154 -8
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +34 -49
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +3 -8
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +59 -19
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +8 -4
- package/lib/worm.js +3 -4
- package/lib/ws-client.js +65 -56
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- 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 (
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
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([
|
|
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
|
-
//
|
|
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
|
|
280
|
-
if (
|
|
281
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
648
|
-
if (
|
|
649
|
-
var k =
|
|
650
|
-
var v =
|
|
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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",
|
|
126
|
+
"skipPaths", "statusOnBlock", "bodyOnBlock", "onDeny", "problemDetails",
|
|
127
|
+
"trustProxy", "trustedProxies", "clientIpResolver", "protocolResolver",
|
|
135
128
|
], "middleware.botGuard");
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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"));
|
package/lib/middleware/cors.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
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,
|
|
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.
|
|
154
|
-
// through so operators behind a TLS terminator
|
|
155
|
-
//
|
|
156
|
-
var reqOrigin = _inferRequestOrigin(req,
|
|
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
|
-
*
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
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", "
|
|
299
|
-
"
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
req.
|
|
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
|