@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
|
@@ -107,7 +107,6 @@ function create(opts) {
|
|
|
107
107
|
}
|
|
108
108
|
var bytesPerDay = opts.bytesPerDay;
|
|
109
109
|
var getKey = typeof opts.getKey === "function" ? opts.getKey : _defaultGetKey;
|
|
110
|
-
var auditOn = opts.audit !== false;
|
|
111
110
|
// onDeny is the canonical hook across the deny-path middleware
|
|
112
111
|
// family; onExceeded is the original name kept working as an alias.
|
|
113
112
|
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny
|
|
@@ -127,33 +126,14 @@ function create(opts) {
|
|
|
127
126
|
});
|
|
128
127
|
var backend = quota._backend;
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (qpos !== -1) p = p.slice(0, qpos);
|
|
135
|
-
for (var i = 0; i < skipPaths.length; i++) {
|
|
136
|
-
if (typeof skipPaths[i] === "string" && p === skipPaths[i]) return true;
|
|
137
|
-
if (skipPaths[i] instanceof RegExp && skipPaths[i].test(p)) return true;
|
|
138
|
-
}
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
129
|
+
// exact:true preserves this guard's whole-path skip semantics (no descendant
|
|
130
|
+
// match); makeSkipMatcher strips the query + resolves url || originalUrl.
|
|
131
|
+
var _shouldSkip = requestHelpers().makeSkipMatcher(
|
|
132
|
+
{ skipPaths: skipPaths, exact: true }, "middleware.dailyByteQuota");
|
|
141
133
|
|
|
142
|
-
|
|
143
|
-
if (!auditOn) return;
|
|
144
|
-
try {
|
|
145
|
-
audit().safeEmit({
|
|
146
|
-
action: "middleware.daily_byte_quota." + action,
|
|
147
|
-
outcome: outcome,
|
|
148
|
-
metadata: metadata || {},
|
|
149
|
-
});
|
|
150
|
-
} catch (_e) { /* drop-silent — audit is best-effort */ }
|
|
151
|
-
}
|
|
134
|
+
var _emitAudit = audit().namespaced("middleware.daily_byte_quota", opts.audit);
|
|
152
135
|
|
|
153
|
-
|
|
154
|
-
try { observability().safeEvent("middleware.daily_byte_quota." + verb, n || 1, labels || {}); }
|
|
155
|
-
catch (_e) { /* drop-silent */ }
|
|
156
|
-
}
|
|
136
|
+
var _emitMetric = observability().namespaced("middleware.daily_byte_quota");
|
|
157
137
|
|
|
158
138
|
return async function dailyByteQuotaMiddleware(req, res, next) {
|
|
159
139
|
if (_shouldSkip(req)) return next();
|
|
@@ -207,13 +187,15 @@ function create(opts) {
|
|
|
207
187
|
// is observed via writableLength as res.write / res.end fire.
|
|
208
188
|
var inboundBytes = 0;
|
|
209
189
|
if (req.headers && typeof req.headers === "object") {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// byte
|
|
190
|
+
// Each header line is "Name: Value\r\n". Count UTF-8 BYTES (not .length
|
|
191
|
+
// / UTF-16 code units) so a multibyte header value isn't under-accounted
|
|
192
|
+
// against the byte quota — matching the outbound counting below, which
|
|
193
|
+
// already uses Buffer.byteLength.
|
|
213
194
|
var keys = Object.keys(req.headers);
|
|
214
195
|
for (var hi = 0; hi < keys.length; hi++) {
|
|
215
196
|
var v = req.headers[keys[hi]];
|
|
216
|
-
inboundBytes += keys[hi]
|
|
197
|
+
inboundBytes += Buffer.byteLength(keys[hi], "utf8") + 2 +
|
|
198
|
+
(typeof v === "string" ? Buffer.byteLength(v, "utf8") : 0) + 2; // ": " + "\r\n" overhead
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
if (req.headers && req.headers["content-length"]) {
|
|
@@ -147,6 +147,24 @@ function denyResponse(req, res, ctx) {
|
|
|
147
147
|
return undefined;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// methodNotAllowed(res, allow) — write the bare HTTP 405 refusal the
|
|
151
|
+
// single-file static-content middlewares (assetlinks / security.txt / web-app-
|
|
152
|
+
// manifest) share: an `Allow` header plus a `text/plain` "Method Not Allowed"
|
|
153
|
+
// body with an explicit Content-Length. Kept separate from denyResponse() (the
|
|
154
|
+
// hook-aware, problem+json-capable refusal path) so the static handlers emit a
|
|
155
|
+
// fixed, minimal response — `allow` is the comma-joined method list (e.g.
|
|
156
|
+
// "GET, HEAD").
|
|
157
|
+
function methodNotAllowed(res, allow) {
|
|
158
|
+
var bodyMsg = "Method Not Allowed";
|
|
159
|
+
res.writeHead(405, { // HTTP 405 status
|
|
160
|
+
"Allow": allow,
|
|
161
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
162
|
+
"Content-Length": Buffer.byteLength(bodyMsg),
|
|
163
|
+
});
|
|
164
|
+
res.end(bodyMsg);
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
module.exports = {
|
|
151
|
-
denyResponse:
|
|
168
|
+
denyResponse: denyResponse,
|
|
169
|
+
methodNotAllowed: methodNotAllowed,
|
|
152
170
|
};
|
|
@@ -42,6 +42,10 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
42
42
|
var observability = lazyRequire(function () { return require("../observability"); });
|
|
43
43
|
|
|
44
44
|
var DEFAULT_METHODS = Object.freeze(["POST", "PUT", "DELETE", "PATCH"]);
|
|
45
|
+
// Monotonic per-process counter giving each fetchMetadata mount a unique
|
|
46
|
+
// idempotency id (see GATE_ID in create) so a stricter sub-route instance is
|
|
47
|
+
// not silently disabled by an earlier lenient one sharing a global flag.
|
|
48
|
+
var _fmGateSeq = 0;
|
|
45
49
|
|
|
46
50
|
// Sec-Fetch-Dest request-destination vocabulary (Fetch Standard §3.2.6
|
|
47
51
|
// "destination", https://fetch.spec.whatwg.org/#concept-request-destination;
|
|
@@ -81,7 +85,7 @@ function _validateDestList(list, label) {
|
|
|
81
85
|
// the typo surfaces before it silently fails to match at request time.
|
|
82
86
|
if (!Array.isArray(list)) return;
|
|
83
87
|
for (var i = 0; i < list.length; i += 1) {
|
|
84
|
-
if (!KNOWN_DEST_SET
|
|
88
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN_DEST_SET, list[i])) {
|
|
85
89
|
throw new Error("middleware.fetchMetadata: " + label + "[" + i +
|
|
86
90
|
"] is not a known Sec-Fetch-Dest value (got '" + String(list[i]) +
|
|
87
91
|
"'). Known destinations: " + KNOWN_DESTINATIONS.join(", ") + ".");
|
|
@@ -165,6 +169,7 @@ function create(opts) {
|
|
|
165
169
|
"allowSameSite", "allowCrossSite", "allowMissing",
|
|
166
170
|
"allowedDest", "deniedDest", "allowStorageAccess", "strictDest",
|
|
167
171
|
"allowedNavigate", "methods", "audit", "onDeny", "problemDetails",
|
|
172
|
+
"skipPaths", "skip",
|
|
168
173
|
], "middleware.fetchMetadata");
|
|
169
174
|
validateOpts.optionalBoolean(opts.allowStorageAccess, "middleware.fetchMetadata: allowStorageAccess");
|
|
170
175
|
validateOpts.optionalBoolean(opts.strictDest, "middleware.fetchMetadata: strictDest");
|
|
@@ -174,6 +179,11 @@ function create(opts) {
|
|
|
174
179
|
_validateDestList(opts.deniedDest, "deniedDest");
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
// Per-path exemption (string-prefix / RegExp / skip predicate), validated at
|
|
183
|
+
// create() — exempt a webhook / cookieless edge-cached route from the
|
|
184
|
+
// fetch-metadata gate without disabling the app-level mount.
|
|
185
|
+
var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.fetchMetadata");
|
|
186
|
+
|
|
177
187
|
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
178
188
|
var problemMode = opts.problemDetails === true;
|
|
179
189
|
var allowSameSite = opts.allowSameSite !== false;
|
|
@@ -200,8 +210,26 @@ function create(opts) {
|
|
|
200
210
|
}
|
|
201
211
|
}
|
|
202
212
|
var allowedNavigate = opts.allowedNavigate !== false;
|
|
213
|
+
// An empty methods array is truthy, so `opts.methods || DEFAULT_METHODS`
|
|
214
|
+
// would keep `[]` and `methods.indexOf(req.method) === -1` would be true for
|
|
215
|
+
// EVERY request — silently turning the gate into a pass-through. Reject a
|
|
216
|
+
// present-but-empty/garbage list at config time (fail-fast, don't degrade
|
|
217
|
+
// the gate to a no-op).
|
|
218
|
+
if (opts.methods !== undefined) {
|
|
219
|
+
if (!Array.isArray(opts.methods) || opts.methods.length === 0 ||
|
|
220
|
+
!opts.methods.every(function (m) { return typeof m === "string" && m.length > 0; })) {
|
|
221
|
+
throw new Error("middleware.fetchMetadata: opts.methods must be a non-empty array of HTTP method tokens (omit it for the POST/PUT/DELETE/PATCH default)");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
203
224
|
var methods = (opts.methods || DEFAULT_METHODS).map(function (m) { return m.toUpperCase(); });
|
|
204
225
|
var auditOn = opts.audit !== false;
|
|
226
|
+
// Per-instance idempotency id: a request carries a Set of gates that have
|
|
227
|
+
// already run. The OLD shared `req._fetchMetadataChecked` boolean let the
|
|
228
|
+
// FIRST fetch-metadata mount (e.g. the lenient app-level default) permanently
|
|
229
|
+
// disable a STRICTER instance layered on a sub-route. Keying by a unique id
|
|
230
|
+
// lets each distinct mount run once while the SAME instance mounted twice
|
|
231
|
+
// still no-ops.
|
|
232
|
+
var GATE_ID = "fm:" + (_fmGateSeq++);
|
|
205
233
|
|
|
206
234
|
function _emitDenied(req, reason) {
|
|
207
235
|
if (!auditOn) return;
|
|
@@ -217,9 +245,12 @@ function create(opts) {
|
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
return function fetchMetadata(req, res, next) {
|
|
220
|
-
// Idempotent:
|
|
221
|
-
|
|
222
|
-
req.
|
|
248
|
+
// Idempotent PER INSTANCE: the same mount running twice on a request is a
|
|
249
|
+
// no-op, but a distinct (e.g. stricter sub-route) instance still evaluates.
|
|
250
|
+
if (!req._fetchMetadataGates) req._fetchMetadataGates = Object.create(null);
|
|
251
|
+
if (req._fetchMetadataGates[GATE_ID]) return next();
|
|
252
|
+
req._fetchMetadataGates[GATE_ID] = true;
|
|
253
|
+
if (_shouldSkip(req)) return next();
|
|
223
254
|
if (methods.indexOf(req.method) === -1) return next();
|
|
224
255
|
|
|
225
256
|
var headers = req.headers || {};
|
|
@@ -53,7 +53,7 @@ var cryptoField = require("../crypto-field");
|
|
|
53
53
|
var vault = require("../vault");
|
|
54
54
|
var { defineClass } = require("../framework-error");
|
|
55
55
|
|
|
56
|
-
var
|
|
56
|
+
var auditEmit = require("../audit-emit");
|
|
57
57
|
var problemDetails = lazyRequire(function () { return require("../problem-details"); });
|
|
58
58
|
var C = require("../constants");
|
|
59
59
|
|
|
@@ -533,16 +533,8 @@ function dbStore(opts) {
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
function _validateStore(store, where) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
where + ": store must be an object", true);
|
|
539
|
-
}
|
|
540
|
-
if (typeof store.get !== "function" ||
|
|
541
|
-
typeof store.set !== "function" ||
|
|
542
|
-
typeof store.delete !== "function") {
|
|
543
|
-
throw new IdempotencyError("idempotency/bad-store",
|
|
544
|
-
where + ": store must implement { get, set, delete }", true);
|
|
545
|
-
}
|
|
536
|
+
validateOpts.requireMethods(store, ["get", "set", "delete"],
|
|
537
|
+
where + ": store", IdempotencyError, "idempotency/bad-store", true);
|
|
546
538
|
}
|
|
547
539
|
|
|
548
540
|
function _fingerprintRequest(req, bodyBytes, store) {
|
|
@@ -568,15 +560,7 @@ function _fingerprintRequest(req, bodyBytes, store) {
|
|
|
568
560
|
return nodeCrypto.createHash("sha3-256").update(preimage).digest("hex");
|
|
569
561
|
}
|
|
570
562
|
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
audit().safeEmit({
|
|
574
|
-
action: action,
|
|
575
|
-
outcome: outcome || "success",
|
|
576
|
-
metadata: metadata,
|
|
577
|
-
});
|
|
578
|
-
} catch (_e) { /* best-effort */ }
|
|
579
|
-
}
|
|
563
|
+
var _emitAudit = auditEmit.emit;
|
|
580
564
|
|
|
581
565
|
/**
|
|
582
566
|
* @primitive b.middleware.idempotencyKey
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* the wiki example default).
|
|
17
17
|
*
|
|
18
18
|
* var fence = b.middleware.networkAllowlist({
|
|
19
|
-
* paths:
|
|
20
|
-
* allowedCidrs:
|
|
21
|
-
*
|
|
22
|
-
* denyStatus:
|
|
23
|
-
* denyBody:
|
|
24
|
-
* audit:
|
|
19
|
+
* paths: ["/admin", "/admin/", "/healthz/internal"],
|
|
20
|
+
* allowedCidrs: ["10.0.0.0/8", "192.168.0.0/16", "::1/128"],
|
|
21
|
+
* trustedProxies: ["10.0.0.0/8"], // peer-gate XFF to your proxy range
|
|
22
|
+
* denyStatus: 404, // default — reveal nothing about the gate
|
|
23
|
+
* denyBody: "Not Found", // default
|
|
24
|
+
* audit: b.audit, // default: null — emits network.gate.denied
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
27
|
* router.use(fence);
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
* Behaviour:
|
|
30
30
|
* - The middleware is path-scoped: requests whose pathname doesn't
|
|
31
31
|
* start with any of `paths` pass through unchanged. Hot-path-cheap.
|
|
32
|
-
* - Requests on a gated path get their client IP resolved
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* - Requests on a gated path get their client IP resolved peer-gated:
|
|
33
|
+
* the socket address by default, or — when `trustedProxies` /
|
|
34
|
+
* `clientIpResolver` is set — X-Forwarded-For honored only from a
|
|
35
|
+
* trusted proxy peer. A bare `trustProxy` is refused at construction
|
|
36
|
+
* because it would let a direct caller forge an allowed address.
|
|
35
37
|
* - The IP is checked against the CIDR allowlist using
|
|
36
38
|
* b.ssrfGuard.cidrContains. A miss returns denyStatus + denyBody
|
|
37
39
|
* and audits the rejection. Default 404 hides the gate's
|
|
@@ -84,38 +86,47 @@ function _validateCidr(cidr) {
|
|
|
84
86
|
* authorization prevents unauthorized USERS from reaching sensitive
|
|
85
87
|
* routes; this middleware adds a NETWORK-layer fence so a credential
|
|
86
88
|
* leak doesn't compromise the gate. Path-scoped — requests outside
|
|
87
|
-
* the configured prefixes pass through hot-path-cheap.
|
|
88
|
-
* client IP
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* the configured prefixes pass through hot-path-cheap. Checks the
|
|
90
|
+
* resolved client IP against the CIDR allowlist via
|
|
91
|
+
* `b.ssrfGuard.cidrContains` and refuses misses with HTTP 404 by
|
|
92
|
+
* default (hides the gate from probes). Throws at create-time on
|
|
93
|
+
* malformed opts.
|
|
94
|
+
*
|
|
95
|
+
* Client-IP resolution is peer-gated. By default only the socket
|
|
96
|
+
* address is used — X-Forwarded-For is attacker-forgeable, so trusting
|
|
97
|
+
* it bare would let a direct caller spoof an allowed IP through the
|
|
98
|
+
* gate. Behind a reverse proxy, declare it with `trustedProxies`
|
|
99
|
+
* (CIDRs — XFF is then honored only when the immediate peer is one of
|
|
100
|
+
* them) or own resolution entirely with `clientIpResolver(req)`. A bare
|
|
101
|
+
* `trustProxy` is refused at construction.
|
|
92
102
|
*
|
|
93
103
|
* @opts
|
|
94
104
|
* {
|
|
95
|
-
* paths:
|
|
96
|
-
* allowedCidrs:
|
|
97
|
-
* deniedCidrs:
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
105
|
+
* paths: string[], // pathname prefixes, required
|
|
106
|
+
* allowedCidrs: string[], // required
|
|
107
|
+
* deniedCidrs: string[],
|
|
108
|
+
* trustedProxies: string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-For
|
|
109
|
+
* clientIpResolver: function(req): string|null, // own client-IP resolution
|
|
110
|
+
* denyStatus: number, // default 404
|
|
111
|
+
* denyBody: string, // default "Not Found"
|
|
112
|
+
* audit: object,
|
|
113
|
+
* onDeny: function(req, res, info): void, // own the refusal; info = { status, reason, clientIp, route }
|
|
114
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
|
|
104
115
|
* }
|
|
105
116
|
*
|
|
106
117
|
* @example
|
|
107
118
|
* var b = require("@blamejs/core");
|
|
108
119
|
* var app = b.router.create();
|
|
109
120
|
* app.use(b.middleware.networkAllowlist({
|
|
110
|
-
* paths:
|
|
111
|
-
* allowedCidrs:
|
|
112
|
-
*
|
|
121
|
+
* paths: ["/admin"],
|
|
122
|
+
* allowedCidrs: ["10.0.0.0/8", "::1/128"],
|
|
123
|
+
* trustedProxies: ["10.0.0.0/8"], // your reverse proxy's range
|
|
113
124
|
* }));
|
|
114
125
|
*/
|
|
115
126
|
function create(opts) {
|
|
116
127
|
opts = opts || {};
|
|
117
128
|
validateOpts(opts, [
|
|
118
|
-
"paths", "allowedCidrs", "deniedCidrs", "trustProxy",
|
|
129
|
+
"paths", "allowedCidrs", "deniedCidrs", "trustProxy", "trustedProxies", "clientIpResolver",
|
|
119
130
|
"denyStatus", "denyBody", "audit", "onDeny", "problemDetails",
|
|
120
131
|
], "middleware.networkAllowlist");
|
|
121
132
|
|
|
@@ -148,10 +159,30 @@ function create(opts) {
|
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
// Client-IP resolution for an access-control gate must be peer-gated:
|
|
163
|
+
// a bare `trustProxy` honors X-Forwarded-For from any caller, so a client
|
|
164
|
+
// connecting directly can forge an allowed address and walk through the
|
|
165
|
+
// gate. Operators behind a reverse proxy declare it via `trustedProxies`
|
|
166
|
+
// (CIDRs of their proxies — XFF is then peer-gated) or own resolution
|
|
167
|
+
// entirely via `clientIpResolver`. We refuse, at construction, the
|
|
168
|
+
// spoofable combination of `trustProxy` without either.
|
|
169
|
+
var _ipResolver;
|
|
170
|
+
try {
|
|
171
|
+
_ipResolver = requestHelpers.trustedClientIp({
|
|
172
|
+
trustedProxies: opts.trustedProxies,
|
|
173
|
+
clientIpResolver: opts.clientIpResolver,
|
|
174
|
+
});
|
|
175
|
+
} catch (e) { throw _err("BAD_OPT", e.message); }
|
|
176
|
+
var trustProxyOpt = opts.trustProxy === true || typeof opts.trustProxy === "number";
|
|
177
|
+
if (trustProxyOpt && !_ipResolver.peerGated) {
|
|
178
|
+
throw _err("BAD_OPT",
|
|
179
|
+
"trustProxy is spoofable for an access-control gate — X-Forwarded-For from a " +
|
|
180
|
+
"direct caller would be trusted. Declare your reverse proxies via " +
|
|
181
|
+
"trustedProxies: [\"10.0.0.0/8\", …] (peer-gated XFF) or supply clientIpResolver(req).");
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
var paths = opts.paths.slice();
|
|
152
185
|
var allowedCidrs = opts.allowedCidrs.slice();
|
|
153
|
-
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
154
|
-
? opts.trustProxy : false;
|
|
155
186
|
var denyStatus = typeof opts.denyStatus === "number" ? opts.denyStatus : 404;
|
|
156
187
|
if (denyStatus < 400 || denyStatus >= 600 || Math.floor(denyStatus) !== denyStatus) {
|
|
157
188
|
throw _err("BAD_OPT", "denyStatus must be a 4xx or 5xx integer, got " + denyStatus);
|
|
@@ -212,7 +243,7 @@ function create(opts) {
|
|
|
212
243
|
var pathname = req.pathname || (req.url || "").split("?")[0];
|
|
213
244
|
if (!_matchesPath(pathname)) return next();
|
|
214
245
|
|
|
215
|
-
var ip =
|
|
246
|
+
var ip = _ipResolver.resolve(req);
|
|
216
247
|
if (!ip) {
|
|
217
248
|
// Fail closed: a request we can't even derive an IP for shouldn't
|
|
218
249
|
// bypass the gate.
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
*
|
|
57
57
|
* Audit: every limit hit emits system.ratelimit.block with the key + path.
|
|
58
58
|
*/
|
|
59
|
+
var boundedMap = require("../bounded-map");
|
|
59
60
|
var C = require("../constants");
|
|
60
61
|
var frameworkSchema = require("../framework-schema");
|
|
61
62
|
var lazyRequire = require("../lazy-require");
|
|
@@ -117,17 +118,6 @@ function _conflictRefs(dialect, table) {
|
|
|
117
118
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
118
119
|
var logger = lazyRequire(function () { return require("../log").boot("rate-limit"); });
|
|
119
120
|
|
|
120
|
-
// `_clientIp` defers to `requestHelpers.clientIp`, threading the
|
|
121
|
-
// per-middleware `trustProxy` opt. Default refuses forwarded headers
|
|
122
|
-
// (returning the socket address only) — operators behind a sanitizing
|
|
123
|
-
// reverse proxy opt in via `trustProxy: true` (or a hop count).
|
|
124
|
-
function _clientIpFor(trustProxy) {
|
|
125
|
-
return function (req) {
|
|
126
|
-
var ip = requestHelpers.clientIp(req, { trustProxy: trustProxy });
|
|
127
|
-
return ip || "unknown";
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
// Reject NaN / Infinity / negative / non-positive / non-number at create
|
|
132
122
|
// time so a misconfigured rate-limit can't silently degrade to "no
|
|
133
123
|
// limit" or produce divide-by-zero verdicts at request time.
|
|
@@ -178,11 +168,14 @@ function _memoryTokenBucketBackend(opts) {
|
|
|
178
168
|
// microtask cost per request.
|
|
179
169
|
function take(key, _cost) {
|
|
180
170
|
var now = Date.now();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
// Insert a fresh full bucket on first sight; only an already-present
|
|
172
|
+
// bucket refills (a brand-new bucket starts at `burst`, so refilling
|
|
173
|
+
// it would be a no-op clamped to `burst` anyway).
|
|
174
|
+
var existed = buckets.has(key);
|
|
175
|
+
var b = boundedMap.getOrInsert(buckets, key, function () {
|
|
176
|
+
return { tokens: burst, lastRefillAt: now };
|
|
177
|
+
});
|
|
178
|
+
if (existed) {
|
|
186
179
|
var elapsed = (now - b.lastRefillAt) / C.TIME.seconds(1);
|
|
187
180
|
b.tokens = Math.min(burst, b.tokens + elapsed * refillPerSecond);
|
|
188
181
|
b.lastRefillAt = now;
|
|
@@ -254,10 +247,15 @@ function _memoryFixedWindowBackend(opts) {
|
|
|
254
247
|
function take(key, _cost) {
|
|
255
248
|
var now = Date.now();
|
|
256
249
|
var windowStart = Math.floor(now / windowMs) * windowMs;
|
|
257
|
-
var c =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
var c = boundedMap.getOrInsert(counters, key, function () {
|
|
251
|
+
return { windowStart: windowStart, count: 0 };
|
|
252
|
+
});
|
|
253
|
+
// A key carried over from a prior window re-seeds to the current
|
|
254
|
+
// window (count restarts) — getOrInsert only handles first-sight, so
|
|
255
|
+
// the rollover case resets the existing record in place.
|
|
256
|
+
if (c.windowStart !== windowStart) {
|
|
257
|
+
c.windowStart = windowStart;
|
|
258
|
+
c.count = 0;
|
|
261
259
|
}
|
|
262
260
|
c.count += 1;
|
|
263
261
|
if (c.count <= max) {
|
|
@@ -448,7 +446,9 @@ function _resolveBackend(opts) {
|
|
|
448
446
|
* limit: number,
|
|
449
447
|
* windowMs: number,
|
|
450
448
|
* pruneIntervalMs: number,
|
|
451
|
-
*
|
|
449
|
+
* trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-For for the IP key
|
|
450
|
+
* clientIpResolver: function(req): string|null, // own the rate-limit key's client IP
|
|
451
|
+
* trustProxy: boolean|number, // legacy; refused with the default IP key (spoofable) — use trustedProxies
|
|
452
452
|
* }
|
|
453
453
|
*
|
|
454
454
|
* @example
|
|
@@ -466,15 +466,33 @@ function create(opts) {
|
|
|
466
466
|
validateOpts(opts, [
|
|
467
467
|
"keyFn", "statusOnLimit", "bodyOnLimit", "onDeny", "problemDetails",
|
|
468
468
|
"header", "headerPrefix", "skipPaths", "scope",
|
|
469
|
-
"backend", "trustProxy", "algorithm",
|
|
469
|
+
"backend", "trustProxy", "trustedProxies", "clientIpResolver", "algorithm",
|
|
470
470
|
// memory backend (token-bucket)
|
|
471
471
|
"burst", "refillPerSecond",
|
|
472
472
|
// memory backend (fixed-window) + cluster backend
|
|
473
473
|
"max", "limit", "windowMs", "pruneIntervalMs",
|
|
474
474
|
], "middleware.rateLimit");
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
// Peer-gated client-IP resolution. The default key (and the audit-actor
|
|
476
|
+
// IP) is the client address; a bare trustProxy would let a caller forge
|
|
477
|
+
// X-Forwarded-For to evade their own limit or poison a victim's bucket.
|
|
478
|
+
// Operators behind a proxy declare trustedProxies (CIDRs) or own
|
|
479
|
+
// resolution via clientIpResolver. A bare trustProxy is refused when the
|
|
480
|
+
// default IP key is in use (no keyFn) — it would be spoofable.
|
|
481
|
+
var _ipResolver;
|
|
482
|
+
try {
|
|
483
|
+
_ipResolver = requestHelpers.trustedClientIp({
|
|
484
|
+
trustedProxies: opts.trustedProxies,
|
|
485
|
+
clientIpResolver: opts.clientIpResolver,
|
|
486
|
+
});
|
|
487
|
+
} catch (e) { throw new Error("middleware.rateLimit: " + e.message); }
|
|
488
|
+
var trustProxyBare = opts.trustProxy === true || typeof opts.trustProxy === "number";
|
|
489
|
+
if (trustProxyBare && !_ipResolver.peerGated && !opts.keyFn) {
|
|
490
|
+
throw new Error("middleware.rateLimit: trustProxy is spoofable — a caller can forge " +
|
|
491
|
+
"X-Forwarded-For to evade the limit or poison another IP's bucket. Declare your " +
|
|
492
|
+
"reverse proxies via trustedProxies: [\"10.0.0.0/8\", …], supply clientIpResolver(req), " +
|
|
493
|
+
"or set your own keyFn.");
|
|
494
|
+
}
|
|
495
|
+
var _clientIp = function (req) { return _ipResolver.resolve(req) || "unknown"; };
|
|
478
496
|
var keyFn = opts.keyFn || _clientIp;
|
|
479
497
|
var statusOnLimit = opts.statusOnLimit || 429;
|
|
480
498
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
@@ -489,29 +507,12 @@ function create(opts) {
|
|
|
489
507
|
? opts.headerPrefix : "X-RateLimit-";
|
|
490
508
|
var limitHeader = headerPrefix + "Limit"; // allow:hand-rolled-sql — HTTP response-header name (X-RateLimit-Limit), not a SQL LIMIT clause
|
|
491
509
|
var remainingHeader = headerPrefix + "Remaining";
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// Anything else would crash _shouldSkip with TypeError on the first request.
|
|
495
|
-
for (var sp = 0; sp < skipPaths.length; sp++) {
|
|
496
|
-
if (typeof skipPaths[sp] !== "string" && !(skipPaths[sp] instanceof RegExp)) {
|
|
497
|
-
throw new Error("middleware.rateLimit: skipPaths[" + sp +
|
|
498
|
-
"] must be a string prefix or RegExp, got " + typeof skipPaths[sp]);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
510
|
+
// Path-exemption predicate (string-prefix or RegExp), validated at create().
|
|
511
|
+
var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.rateLimit");
|
|
501
512
|
var scope = opts.scope || "global";
|
|
502
513
|
|
|
503
514
|
var backend = _resolveBackend(opts);
|
|
504
515
|
|
|
505
|
-
function _shouldSkip(req) {
|
|
506
|
-
var path = req.pathname || req.url || "/";
|
|
507
|
-
for (var i = 0; i < skipPaths.length; i++) {
|
|
508
|
-
if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
516
|
function _writeBlocked(req, res, k, verdict) {
|
|
516
517
|
if (emitHeaders && typeof res.setHeader === "function") {
|
|
517
518
|
res.setHeader(limitHeader, String(verdict.limit));
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// + /Schemas surfaces backed by operator-supplied CRUD callbacks.
|
|
5
5
|
|
|
6
6
|
var framework_error = require("../framework-error");
|
|
7
|
+
var numericBounds = require("../numeric-bounds");
|
|
8
|
+
var pick = require("../pick");
|
|
7
9
|
var validateOpts = require("../validate-opts");
|
|
8
10
|
var safeJson = require("../safe-json");
|
|
9
11
|
var safeBuffer = require("../safe-buffer");
|
|
@@ -516,7 +518,7 @@ function _walkBulkIdRefs(value, out) {
|
|
|
516
518
|
if (value && typeof value === "object") {
|
|
517
519
|
var keys = Object.keys(value);
|
|
518
520
|
for (var k = 0; k < keys.length; k++) {
|
|
519
|
-
if (keys[k]
|
|
521
|
+
if (pick.isPoisonedKey(keys[k])) continue;
|
|
520
522
|
_walkBulkIdRefs(value[keys[k]], out);
|
|
521
523
|
}
|
|
522
524
|
}
|
|
@@ -608,7 +610,7 @@ function _anyDependencyFailed(refs, plan, executed, bulkIdMap) {
|
|
|
608
610
|
// RFC 7644 §3.7 — failOnErrors is an OPTIONAL integer >= 1. Absent /
|
|
609
611
|
// non-conforming values mean "process every operation" (null).
|
|
610
612
|
function _parseFailOnErrors(value) {
|
|
611
|
-
if (
|
|
613
|
+
if (!numericBounds.isPositiveFiniteInt(value)) {
|
|
612
614
|
return null;
|
|
613
615
|
}
|
|
614
616
|
return value;
|
|
@@ -753,7 +755,7 @@ function _resolveBulkIdRefs(value, bulkIdMap) {
|
|
|
753
755
|
var out = {};
|
|
754
756
|
var keys = Object.keys(value);
|
|
755
757
|
for (var k = 0; k < keys.length; k++) {
|
|
756
|
-
if (keys[k]
|
|
758
|
+
if (pick.isPoisonedKey(keys[k])) continue;
|
|
757
759
|
out[keys[k]] = _resolveBulkIdRefs(value[keys[k]], bulkIdMap);
|
|
758
760
|
}
|
|
759
761
|
return out;
|
|
@@ -151,7 +151,15 @@ var PP_POLICY_RE =
|
|
|
151
151
|
/^[a-z][a-z0-9-]*=(?:\*|\([^)]*\)|self)$/;
|
|
152
152
|
function _validatePermissionsPolicy(value) {
|
|
153
153
|
if (typeof value !== "string" || value.length === 0) return;
|
|
154
|
-
|
|
154
|
+
// Split on the literal comma, then strip whitespace adjacent to each comma —
|
|
155
|
+
// the semantics of /\s*,\s*/ without that pattern's O(n^2) backtracking on a
|
|
156
|
+
// long comma-less run of whitespace. trimStart/trimEnd use the same Unicode
|
|
157
|
+
// whitespace set as \s.
|
|
158
|
+
var parts = String(value).split(",");
|
|
159
|
+
for (var s = 0; s < parts.length; s += 1) {
|
|
160
|
+
if (s > 0) parts[s] = parts[s].trimStart();
|
|
161
|
+
if (s < parts.length - 1) parts[s] = parts[s].trimEnd();
|
|
162
|
+
}
|
|
155
163
|
for (var i = 0; i < parts.length; i += 1) {
|
|
156
164
|
var p = parts[i];
|
|
157
165
|
if (!p) continue;
|
|
@@ -202,7 +210,9 @@ function _validatePermissionsPolicy(value) {
|
|
|
202
210
|
* acceptCh: string|false,
|
|
203
211
|
* criticalCh: string|false,
|
|
204
212
|
* reportingEndpoints: object,
|
|
205
|
-
*
|
|
213
|
+
* trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-Proto for HSTS
|
|
214
|
+
* protocolResolver: function(req): "http"|"https", // own the HTTPS decision
|
|
215
|
+
* trustProxy: boolean|number, // legacy; refused unless paired with trustedProxies/protocolResolver (spoofable)
|
|
206
216
|
* coopReportOnly: string, // default: off — monitor-mode COOP
|
|
207
217
|
* coepReportOnly: string, // default: off — monitor-mode COEP
|
|
208
218
|
* documentPolicyReportOnly: string, // default: off — monitor-mode Document-Policy
|
|
@@ -223,6 +233,7 @@ function create(opts) {
|
|
|
223
233
|
"hsts", "contentTypeOptions", "frameOptions", "referrerPolicy",
|
|
224
234
|
"permissionsPolicy", "coop", "coep", "corp",
|
|
225
235
|
"originAgentCluster", "dnsPrefetchControl", "csp", "trustProxy",
|
|
236
|
+
"trustedProxies", "protocolResolver",
|
|
226
237
|
"reportingEndpoints", "documentPolicy", "criticalCh", "acceptCh",
|
|
227
238
|
"coopReportOnly", "coepReportOnly", "documentPolicyReportOnly",
|
|
228
239
|
"requireDocumentPolicy", "serviceWorkerAllowed",
|
|
@@ -230,8 +241,23 @@ function create(opts) {
|
|
|
230
241
|
if (opts.permissionsPolicy && typeof opts.permissionsPolicy === "string") {
|
|
231
242
|
_validatePermissionsPolicy(opts.permissionsPolicy);
|
|
232
243
|
}
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
// HSTS is emitted only on HTTPS responses; behind a proxy that comes from
|
|
245
|
+
// X-Forwarded-Proto. A bare trustProxy trusts the forgeable header from any
|
|
246
|
+
// caller, so a direct request forging "http" could suppress HSTS on a real
|
|
247
|
+
// HTTPS response (SSL-strip window). Peer-gate it via trustedProxies, or own
|
|
248
|
+
// the decision via protocolResolver; a bare trustProxy is refused.
|
|
249
|
+
var _proto;
|
|
250
|
+
try {
|
|
251
|
+
_proto = requestHelpers.trustedProtocol({
|
|
252
|
+
trustedProxies: opts.trustedProxies,
|
|
253
|
+
protocolResolver: opts.protocolResolver,
|
|
254
|
+
});
|
|
255
|
+
} catch (e) { throw new TypeError("middleware.securityHeaders: " + e.message); }
|
|
256
|
+
if ((opts.trustProxy === true || typeof opts.trustProxy === "number") && !_proto.peerGated) {
|
|
257
|
+
throw new TypeError("middleware.securityHeaders: trustProxy is spoofable for the HSTS " +
|
|
258
|
+
"decision — a direct caller could forge X-Forwarded-Proto to suppress HSTS. Declare your " +
|
|
259
|
+
"reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] or supply protocolResolver(req).");
|
|
260
|
+
}
|
|
235
261
|
var hsts = opts.hsts === undefined ? "max-age=63072000; includeSubDomains; preload" : opts.hsts;
|
|
236
262
|
var ctOpts = opts.contentTypeOptions === undefined ? "nosniff" : opts.contentTypeOptions;
|
|
237
263
|
var frameOpts = opts.frameOptions === undefined ? "DENY" : opts.frameOptions;
|
|
@@ -309,9 +335,9 @@ function create(opts) {
|
|
|
309
335
|
// RFC 6797 §7.2: HSTS over plain HTTP is meaningless (UAs ignore
|
|
310
336
|
// it). Skip the header on non-TLS requests so dev-over-HTTP doesn't
|
|
311
337
|
// surface confusing "Strict-Transport-Security on http://" lines.
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
if (hsts &&
|
|
338
|
+
// Peer-gated protocol resolution — X-Forwarded-Proto honored only from a
|
|
339
|
+
// trusted proxy (trustedProxies / protocolResolver), else the TLS socket.
|
|
340
|
+
if (hsts && _proto.resolve(req) === "https") {
|
|
315
341
|
res.setHeader("Strict-Transport-Security", hsts);
|
|
316
342
|
}
|
|
317
343
|
if (ctOpts) res.setHeader("X-Content-Type-Options", ctOpts);
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
var lazyRequire = require("../lazy-require");
|
|
29
29
|
var validateOpts = require("../validate-opts");
|
|
30
|
+
var denyResponse = require("./deny-response");
|
|
30
31
|
var { defineClass } = require("../framework-error");
|
|
31
32
|
|
|
32
33
|
var SecurityTxtError = defineClass("SecurityTxtError", { alwaysPermanent: true });
|
|
@@ -143,12 +144,7 @@ function create(opts) {
|
|
|
143
144
|
(alsoAtRoot && path === "/security.txt");
|
|
144
145
|
if (!matches) return next();
|
|
145
146
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
146
|
-
|
|
147
|
-
"Allow": "GET, HEAD",
|
|
148
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
149
|
-
"Content-Length": 18, // len of "Method Not Allowed"
|
|
150
|
-
});
|
|
151
|
-
res.end("Method Not Allowed");
|
|
147
|
+
denyResponse.methodNotAllowed(res, "GET, HEAD");
|
|
152
148
|
return;
|
|
153
149
|
}
|
|
154
150
|
res.writeHead(200, { // HTTP 200 status
|