@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
package/lib/redact.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
var C = require("./constants");
|
|
40
|
+
var safeBuffer = require("./safe-buffer");
|
|
40
41
|
var lazyRequire = require("./lazy-require");
|
|
41
42
|
var safeJson = require("./safe-json");
|
|
42
43
|
var validateOpts = require("./validate-opts");
|
|
@@ -149,7 +150,7 @@ var VALUE_DETECTORS = [
|
|
|
149
150
|
name: "connection-string",
|
|
150
151
|
test: function (v) {
|
|
151
152
|
if (typeof v !== "string" || v.length < C.BYTES.bytes(8)) return false; // bound BEFORE regex test
|
|
152
|
-
if (v
|
|
153
|
+
if (safeBuffer.byteLengthOf(v) > C.BYTES.kib(8)) return false;
|
|
153
154
|
// user may be empty (e.g. redis://:password@host); password
|
|
154
155
|
// segment is required to flag this as a credentialed URI.
|
|
155
156
|
return /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s:/?#]*:[^\s@/?#]+@/.test(v);
|
|
@@ -277,12 +278,16 @@ function redact(value, opts) {
|
|
|
277
278
|
function _redact(value, depth, maxDepth, marker, parentKey) {
|
|
278
279
|
if (depth > maxDepth) return marker;
|
|
279
280
|
if (value === null || value === undefined) return value;
|
|
281
|
+
// A sensitive parent key collapses the ENTIRE value — scalar OR composite —
|
|
282
|
+
// to the marker. Checking before the type branches is what stops a secret
|
|
283
|
+
// under e.g. `authorization: ["Bearer …"]` / `password: {…}` from slipping
|
|
284
|
+
// through the array/object branches (which recurse with parentKey=null), the
|
|
285
|
+
// way a scalar already does (CWE-532 telemetry egress).
|
|
286
|
+
if (parentKey && _isSensitiveFieldName(parentKey)) return marker;
|
|
280
287
|
if (typeof value === "string") {
|
|
281
|
-
if (parentKey && _isSensitiveFieldName(parentKey)) return marker;
|
|
282
288
|
return _redactValue(value);
|
|
283
289
|
}
|
|
284
290
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
285
|
-
if (parentKey && _isSensitiveFieldName(parentKey)) return marker;
|
|
286
291
|
return value;
|
|
287
292
|
}
|
|
288
293
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
@@ -512,7 +517,7 @@ function classifyDefaults(opts) {
|
|
|
512
517
|
"redact.classifyDefaults: patterns[" + p + "] must be a string, got " +
|
|
513
518
|
typeof patterns[p]);
|
|
514
519
|
}
|
|
515
|
-
if (!CLASSIFIER_PATTERNS
|
|
520
|
+
if (!Object.prototype.hasOwnProperty.call(CLASSIFIER_PATTERNS, patterns[p]) &&
|
|
516
521
|
!(opts.extra && opts.extra[patterns[p]])) {
|
|
517
522
|
throw new DlpError("redact-dlp/unknown-pattern",
|
|
518
523
|
"redact.classifyDefaults: unknown pattern '" + patterns[p] +
|
package/lib/render.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* Server-side HTML / JSON / XML response helpers.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
var validateOpts = require("./validate-opts");
|
|
32
|
+
|
|
31
33
|
var DEFAULT_CHARSET = "utf-8";
|
|
32
34
|
|
|
33
35
|
function _alreadyDone(res) {
|
|
@@ -53,8 +55,8 @@ function _writeResponse(res, status, headers, body) {
|
|
|
53
55
|
function _mergedHeaders(base, extra) {
|
|
54
56
|
if (!extra) return base;
|
|
55
57
|
var out = {};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
validateOpts.assignOwnEnumerable(out, base);
|
|
59
|
+
validateOpts.assignOwnEnumerable(out, extra);
|
|
58
60
|
return out;
|
|
59
61
|
}
|
|
60
62
|
|
package/lib/request-helpers.js
CHANGED
|
@@ -42,6 +42,13 @@
|
|
|
42
42
|
// every consumer reads HTTP_STATUS.<NAME> rather than the underlying
|
|
43
43
|
// integer, so the hex form is purely an internal storage detail.
|
|
44
44
|
var structuredFields = require("./structured-fields");
|
|
45
|
+
var pick = require("./pick");
|
|
46
|
+
var codepointClass = require("./codepoint-class");
|
|
47
|
+
var lazyRequire = require("./lazy-require");
|
|
48
|
+
// Lazy — ssrf-guard pulls in the network/DNS stack, and request-helpers is
|
|
49
|
+
// required very early in the boot graph. Only touched at middleware-construction
|
|
50
|
+
// time by trustedClientIp(), never on the hot path.
|
|
51
|
+
var _ssrfGuard = lazyRequire(function () { return require("./ssrf-guard"); });
|
|
45
52
|
|
|
46
53
|
var HTTP_STATUS = Object.freeze({
|
|
47
54
|
OK: 0xC8,
|
|
@@ -212,15 +219,24 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
|
212
219
|
*
|
|
213
220
|
* Resolve the originating client IP from a request. Default reads
|
|
214
221
|
* only `req.socket.remoteAddress` — `X-Forwarded-For` is ignored
|
|
215
|
-
* because without a sanitizing reverse proxy it's
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* `trustProxy
|
|
219
|
-
*
|
|
220
|
-
*
|
|
222
|
+
* because without a sanitizing reverse proxy it's attacker-forgeable.
|
|
223
|
+
*
|
|
224
|
+
* For an access-control decision (allowlist, rate-limit key, IP-bound
|
|
225
|
+
* grant), pass `trustProxy` as a PREDICATE `function(addr) => boolean`
|
|
226
|
+
* naming your trusted reverse proxies. The header is then honored only
|
|
227
|
+
* when the immediate TCP peer is itself a trusted proxy, and the client
|
|
228
|
+
* is the first untrusted address walking the chain right-to-left. A
|
|
229
|
+
* direct attacker cannot forge it — this is the only peer-gated form.
|
|
230
|
+
*
|
|
231
|
+
* The legacy `trustProxy: true` (leftmost XFF hop) and `trustProxy: <N>`
|
|
232
|
+
* (Nth-from-rightmost) forms do NOT verify the peer: a client connecting
|
|
233
|
+
* directly can forge any value. They are safe only when an upstream you
|
|
234
|
+
* control terminates and rewrites X-Forwarded-For on every request — never
|
|
235
|
+
* for a security decision on an internet-facing listener. Prefer the
|
|
236
|
+
* predicate form. Returns `null` when no address can be read — never throws.
|
|
221
237
|
*
|
|
222
238
|
* @opts
|
|
223
|
-
* trustProxy: boolean | number // false (default) |
|
|
239
|
+
* trustProxy: boolean | number | function // false (default) | predicate (peer-gated) | legacy true/hop-count
|
|
224
240
|
*
|
|
225
241
|
* @example
|
|
226
242
|
* var req = {
|
|
@@ -230,30 +246,55 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
|
230
246
|
* b.requestHelpers.clientIp(req);
|
|
231
247
|
* // → "10.0.0.1" (forwarded headers ignored by default)
|
|
232
248
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
249
|
+
* var fromTrusted = function (a) { return a.indexOf("10.") === 0; };
|
|
250
|
+
* b.requestHelpers.clientIp(req, { trustProxy: fromTrusted });
|
|
251
|
+
* // → "203.0.113.7" (peer 10.0.0.1 trusted; first untrusted hop)
|
|
235
252
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
253
|
+
* var forged = { socket: { remoteAddress: "198.51.100.66" },
|
|
254
|
+
* headers: { "x-forwarded-for": "203.0.113.7" } };
|
|
255
|
+
* b.requestHelpers.clientIp(forged, { trustProxy: fromTrusted });
|
|
256
|
+
* // → "198.51.100.66" (peer untrusted → forged header ignored)
|
|
238
257
|
*
|
|
239
258
|
* b.requestHelpers.clientIp(undefined);
|
|
240
259
|
* // → null
|
|
241
260
|
*/
|
|
242
261
|
function clientIp(req, opts) {
|
|
243
262
|
if (!req) return null;
|
|
263
|
+
var socketAddr =
|
|
264
|
+
(req.socket && typeof req.socket.remoteAddress === "string" && req.socket.remoteAddress) ? req.socket.remoteAddress
|
|
265
|
+
: (req.connection && typeof req.connection.remoteAddress === "string" && req.connection.remoteAddress) ? req.connection.remoteAddress
|
|
266
|
+
: null;
|
|
244
267
|
var trust = opts && opts.trustProxy;
|
|
245
268
|
if (trust && req.headers) {
|
|
246
269
|
var xff = req.headers["x-forwarded-for"];
|
|
247
270
|
if (xff) {
|
|
248
271
|
var hops = parseListHeader(xff);
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
272
|
+
if (hops.length) {
|
|
273
|
+
if (typeof trust === "function") {
|
|
274
|
+
// Peer-gated resolution: `trust(addr)` names the trusted reverse
|
|
275
|
+
// proxies. X-Forwarded-For is honored ONLY when the immediate TCP
|
|
276
|
+
// peer is itself a trusted proxy; the real client is then the first
|
|
277
|
+
// untrusted address walking the chain right-to-left (each hop is
|
|
278
|
+
// appended by the proxy that observed it). A direct attacker — whose
|
|
279
|
+
// socket peer is not a trusted proxy — cannot forge the result: the
|
|
280
|
+
// forgeable header is ignored and we fall through to the socket
|
|
281
|
+
// address. This is the only form safe for an access-control decision.
|
|
282
|
+
if (socketAddr && trust(socketAddr)) {
|
|
283
|
+
for (var i = hops.length - 1; i >= 0; i--) {
|
|
284
|
+
if (!trust(hops[i])) return hops[i];
|
|
285
|
+
}
|
|
286
|
+
return hops[0]; // entire chain trusted — earliest claimed client
|
|
287
|
+
}
|
|
288
|
+
// peer is not a trusted proxy → ignore forgeable XFF, fall through
|
|
289
|
+
} else if (trust === true) {
|
|
290
|
+
return hops[0];
|
|
291
|
+
} else if (typeof trust === "number" && trust >= 1 && hops.length >= trust) {
|
|
292
|
+
return hops[hops.length - trust];
|
|
293
|
+
}
|
|
252
294
|
}
|
|
253
295
|
}
|
|
254
296
|
}
|
|
255
|
-
if (
|
|
256
|
-
if (req.connection && typeof req.connection.remoteAddress === "string") return req.connection.remoteAddress;
|
|
297
|
+
if (socketAddr) return socketAddr;
|
|
257
298
|
// Express-shaped requests expose the resolved client address as `req.ip`
|
|
258
299
|
// (Express derives it from the socket, honoring its own trust-proxy
|
|
259
300
|
// setting) without a `socket.remoteAddress` surface. Fall back to it so a
|
|
@@ -264,6 +305,125 @@ function clientIp(req, opts) {
|
|
|
264
305
|
return null;
|
|
265
306
|
}
|
|
266
307
|
|
|
308
|
+
/**
|
|
309
|
+
* @primitive b.requestHelpers.trustedClientIp
|
|
310
|
+
* @signature b.requestHelpers.trustedClientIp(opts?)
|
|
311
|
+
* @since 0.15.14
|
|
312
|
+
* @related b.requestHelpers.clientIp
|
|
313
|
+
*
|
|
314
|
+
* Build a peer-gated client-IP resolver for an access-control decision
|
|
315
|
+
* (allowlist, rate-limit key, IP-bound grant). The bare `trustProxy`
|
|
316
|
+
* forms of `clientIp` are forgeable; this is the shape every gate shares
|
|
317
|
+
* so the trust model is identical across them. Returns
|
|
318
|
+
* `{ resolve(req), peerGated }`: `resolve` reads the client IP, `peerGated`
|
|
319
|
+
* is true when `trustedProxies` or `clientIpResolver` was supplied — a
|
|
320
|
+
* gate uses it to refuse a bare `trustProxy` at construction (fail closed).
|
|
321
|
+
*
|
|
322
|
+
* With `clientIpResolver(req)` the operator owns resolution entirely. With
|
|
323
|
+
* `trustedProxies` (CIDRs of the reverse proxies), `X-Forwarded-For` is
|
|
324
|
+
* honored only when the immediate peer is one of them. With neither, only
|
|
325
|
+
* the socket address is used and forwarded headers are ignored.
|
|
326
|
+
*
|
|
327
|
+
* @opts
|
|
328
|
+
* trustedProxies: string | string[], // CIDRs — peer-gate X-Forwarded-For
|
|
329
|
+
* clientIpResolver: function(req): string|null, // own resolution entirely
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* var tip = b.requestHelpers.trustedClientIp({ trustedProxies: ["10.0.0.0/8"] });
|
|
333
|
+
* var ip = tip.resolve(req); // peer-gated; forged XFF from a direct caller ignored
|
|
334
|
+
*/
|
|
335
|
+
// Build the trusted-proxy predicate shared by trustedClientIp / trustedProtocol.
|
|
336
|
+
// Validates each CIDR (a CIDR is valid iff it contains its own network address,
|
|
337
|
+
// reusing the same matcher the predicate uses so format rules can't diverge) and
|
|
338
|
+
// returns fn(addr)=>boolean, or null when no trustedProxies were given. `where`
|
|
339
|
+
// names the calling helper for the error message.
|
|
340
|
+
function _trustedProxyPredicate(trustedProxies, where) {
|
|
341
|
+
if (!trustedProxies || !trustedProxies.length) return null;
|
|
342
|
+
var ssrfGuard = _ssrfGuard();
|
|
343
|
+
for (var i = 0; i < trustedProxies.length; i++) {
|
|
344
|
+
var cidr = trustedProxies[i];
|
|
345
|
+
var slash = typeof cidr === "string" ? cidr.indexOf("/") : -1;
|
|
346
|
+
if (slash === -1 || !ssrfGuard.cidrContains(cidr, cidr.slice(0, slash))) {
|
|
347
|
+
throw new TypeError(where + ": trustedProxies[" + i + "] is not a valid CIDR, got " + JSON.stringify(cidr));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return function (addr) {
|
|
351
|
+
// Fold an IPv4-mapped IPv6 peer (::ffff:a.b.c.d, common on a dual-stack
|
|
352
|
+
// listener) to its dotted IPv4 form so it matches an IPv4 trustedProxies
|
|
353
|
+
// CIDR — cidrContains rejects a cross-family compare, so without this a
|
|
354
|
+
// mapped proxy peer reads as untrusted and X-Forwarded-* is ignored. Only
|
|
355
|
+
// the ::ffff:0:0/96 block folds (canonicalizeHost leaves NAT64 / 6to4 as
|
|
356
|
+
// IPv6), so this can't widen the trusted set.
|
|
357
|
+
var canon = ssrfGuard.canonicalizeHost(addr);
|
|
358
|
+
for (var j = 0; j < trustedProxies.length; j++) {
|
|
359
|
+
if (ssrfGuard.cidrContains(trustedProxies[j], canon)) return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function _normTrustedProxies(opts) {
|
|
366
|
+
return Array.isArray(opts.trustedProxies) ? opts.trustedProxies.slice()
|
|
367
|
+
: (typeof opts.trustedProxies === "string" && opts.trustedProxies.length ? [opts.trustedProxies] : []);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function trustedClientIp(opts) {
|
|
371
|
+
opts = opts || {};
|
|
372
|
+
var resolver = opts.clientIpResolver;
|
|
373
|
+
if (resolver != null && typeof resolver !== "function") {
|
|
374
|
+
throw new TypeError("trustedClientIp: clientIpResolver must be a function(req) => ip|null");
|
|
375
|
+
}
|
|
376
|
+
var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedClientIp");
|
|
377
|
+
return {
|
|
378
|
+
peerGated: !!(resolver || predicate),
|
|
379
|
+
resolve: function (req) {
|
|
380
|
+
if (resolver) return resolver(req);
|
|
381
|
+
if (predicate) return clientIp(req, { trustProxy: predicate });
|
|
382
|
+
return clientIp(req, { trustProxy: false });
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @primitive b.requestHelpers.trustedProtocol
|
|
389
|
+
* @signature b.requestHelpers.trustedProtocol(opts?)
|
|
390
|
+
* @since 0.15.14
|
|
391
|
+
* @related b.requestHelpers.requestProtocol, b.requestHelpers.trustedClientIp
|
|
392
|
+
*
|
|
393
|
+
* Peer-gated companion to trustedClientIp for the request scheme. The
|
|
394
|
+
* Secure-cookie / HSTS / secure-context decisions hinge on whether a request
|
|
395
|
+
* arrived over HTTPS; behind a TLS-terminating proxy that comes from
|
|
396
|
+
* X-Forwarded-Proto, which is forgeable unless the immediate peer is a trusted
|
|
397
|
+
* proxy. Returns `{ resolve(req)=>"http"|"https", peerGated }`. With
|
|
398
|
+
* `trustedProxies` (CIDRs) the header is honored only from a trusted peer; with
|
|
399
|
+
* `protocolResolver(req)` the operator owns the decision; with neither only the
|
|
400
|
+
* real TLS socket is consulted (forwarded headers ignored).
|
|
401
|
+
*
|
|
402
|
+
* @opts
|
|
403
|
+
* trustedProxies: string | string[],
|
|
404
|
+
* protocolResolver: function(req): "http"|"https",
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* var tp = b.requestHelpers.trustedProtocol({ trustedProxies: ["10.0.0.0/8"] });
|
|
408
|
+
* tp.resolve(req); // "https" only when X-Forwarded-Proto came via a trusted peer
|
|
409
|
+
*/
|
|
410
|
+
function trustedProtocol(opts) {
|
|
411
|
+
opts = opts || {};
|
|
412
|
+
var resolver = opts.protocolResolver;
|
|
413
|
+
if (resolver != null && typeof resolver !== "function") {
|
|
414
|
+
throw new TypeError("trustedProtocol: protocolResolver must be a function(req) => 'http'|'https'");
|
|
415
|
+
}
|
|
416
|
+
var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedProtocol");
|
|
417
|
+
return {
|
|
418
|
+
peerGated: !!(resolver || predicate),
|
|
419
|
+
resolve: function (req) {
|
|
420
|
+
if (resolver) return resolver(req);
|
|
421
|
+
if (predicate) return requestProtocol(req, { trustProxy: predicate });
|
|
422
|
+
return requestProtocol(req, { trustProxy: false });
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
267
427
|
/**
|
|
268
428
|
* @primitive b.requestHelpers.requestProtocol
|
|
269
429
|
* @signature b.requestHelpers.requestProtocol(req, opts?)
|
|
@@ -272,14 +432,17 @@ function clientIp(req, opts) {
|
|
|
272
432
|
*
|
|
273
433
|
* Resolve the inbound transport scheme. Default returns `"https"`
|
|
274
434
|
* when `req.socket.encrypted` is set, otherwise `"http"`. Behind a
|
|
275
|
-
* trusted reverse proxy that terminates TLS,
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
435
|
+
* trusted reverse proxy that terminates TLS, pass `trustProxy` as a
|
|
436
|
+
* PREDICATE `function(addr)=>boolean` naming your proxies:
|
|
437
|
+
* `X-Forwarded-Proto` is then honored only when the immediate peer is
|
|
438
|
+
* a trusted proxy, so a direct caller can't forge it (use
|
|
439
|
+
* `b.requestHelpers.trustedProtocol` to build this). The legacy
|
|
440
|
+
* `trustProxy: true` reads the leftmost hop without checking the peer —
|
|
441
|
+
* forgeable, safe only behind an edge that rewrites the header. Always
|
|
442
|
+
* returns a string; on bad input falls back to `"http"`.
|
|
280
443
|
*
|
|
281
444
|
* @opts
|
|
282
|
-
* trustProxy: boolean // false (default) | true
|
|
445
|
+
* trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true
|
|
283
446
|
*
|
|
284
447
|
* @example
|
|
285
448
|
* var req = { socket: { encrypted: true } };
|
|
@@ -303,7 +466,22 @@ function requestProtocol(req, opts) {
|
|
|
303
466
|
var fwd = req.headers["x-forwarded-proto"];
|
|
304
467
|
if (typeof fwd === "string" && fwd.length > 0) {
|
|
305
468
|
var hops = parseListHeader(fwd, { lowercase: true });
|
|
306
|
-
if (hops.length > 0)
|
|
469
|
+
if (hops.length > 0) {
|
|
470
|
+
if (typeof trust === "function") {
|
|
471
|
+
// Peer-gated: honor X-Forwarded-Proto only when the immediate TCP
|
|
472
|
+
// peer is a trusted proxy. A direct caller's forged header is
|
|
473
|
+
// ignored — fall through to the real TLS socket. The only form safe
|
|
474
|
+
// for a Secure-cookie / HSTS / secure-context decision.
|
|
475
|
+
var peer =
|
|
476
|
+
(req.socket && typeof req.socket.remoteAddress === "string" && req.socket.remoteAddress) ? req.socket.remoteAddress
|
|
477
|
+
: (req.connection && typeof req.connection.remoteAddress === "string" && req.connection.remoteAddress) ? req.connection.remoteAddress
|
|
478
|
+
: null;
|
|
479
|
+
if (peer && trust(peer)) return hops[0];
|
|
480
|
+
// peer not a trusted proxy → ignore forgeable header, fall through
|
|
481
|
+
} else {
|
|
482
|
+
return hops[0]; // legacy true/number — spoofable, see docstring
|
|
483
|
+
}
|
|
484
|
+
}
|
|
307
485
|
}
|
|
308
486
|
}
|
|
309
487
|
if (req.socket && req.socket.encrypted) return "https";
|
|
@@ -468,6 +646,91 @@ function resolveRoute(req) {
|
|
|
468
646
|
return qIdx === -1 ? url : url.slice(0, qIdx);
|
|
469
647
|
}
|
|
470
648
|
|
|
649
|
+
/**
|
|
650
|
+
* @primitive b.requestHelpers.makeSkipMatcher
|
|
651
|
+
* @signature b.requestHelpers.makeSkipMatcher(opts, label)
|
|
652
|
+
* @since 0.15.13
|
|
653
|
+
* @status stable
|
|
654
|
+
* @related b.requestHelpers.resolveRoute
|
|
655
|
+
*
|
|
656
|
+
* Build a `(req) => boolean` path-match predicate shared by the state-change
|
|
657
|
+
* guards (`csrfProtect` / `fetchMetadata` / `botGuard` / `rateLimit`) AND the
|
|
658
|
+
* route-exemption / mount checks in `auth.accessLock`, `middleware.ageGate`,
|
|
659
|
+
* `middleware.botDisclose`, and `middleware.dailyByteQuota` — so a single route
|
|
660
|
+
* can be exempted (or a middleware mounted on a path subset) without each caller
|
|
661
|
+
* re-rolling the loop. `opts.skipPaths` entries are validated at build time —
|
|
662
|
+
* each must be a string or a RegExp — so an operator typo dies at boot, not on
|
|
663
|
+
* the first request; the optional `opts.skip(req)` predicate is validated the
|
|
664
|
+
* same way.
|
|
665
|
+
*
|
|
666
|
+
* A STRING entry matches on a SEGMENT BOUNDARY, not a raw prefix: `"/api"`
|
|
667
|
+
* matches `/api` and `/api/x` but NOT `/apixyz` — a raw `startsWith` would skip
|
|
668
|
+
* the guard on an unintended sibling path (a guard-bypass class). An entry that
|
|
669
|
+
* already ends in `/` is itself a segment prefix. Pass `exact: true` to require
|
|
670
|
+
* a whole-path match (no descendant). A RegExp entry uses `.test(path)`. The
|
|
671
|
+
* tested path is `req.pathname || req.url || req.originalUrl || "/"` with the
|
|
672
|
+
* query string stripped (matching is on the path, never the query). A `skip`
|
|
673
|
+
* predicate that throws is treated as "do not skip", so a buggy exemption can
|
|
674
|
+
* only keep the guard ON, never silently bypass it.
|
|
675
|
+
*
|
|
676
|
+
* @opts
|
|
677
|
+
* skipPaths: Array<string|RegExp>, // string = segment-boundary match; RegExp = .test(path)
|
|
678
|
+
* exact: boolean, // string entries match whole-path only (no descendant). default false
|
|
679
|
+
* skip: function, // (req) => boolean, optional route-aware predicate
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* var shouldSkip = b.requestHelpers.makeSkipMatcher(
|
|
683
|
+
* { skipPaths: ["/healthz", /^\/webhooks\//] }, "middleware.csrfProtect");
|
|
684
|
+
* if (shouldSkip(req)) return next();
|
|
685
|
+
*/
|
|
686
|
+
// _skipStrMatch — does the request path match a single STRING skip entry?
|
|
687
|
+
// SEGMENT-BOUNDARY semantics, NOT a raw `startsWith`: entry "/api" matches
|
|
688
|
+
// "/api" and "/api/x" but NOT "/apixyz" (a raw prefix would wrongly skip the
|
|
689
|
+
// guard on the sibling path — a guard-bypass class). An entry that already
|
|
690
|
+
// ends in "/" is itself a segment prefix ("/webhooks/" matches "/webhooks/x").
|
|
691
|
+
// `exact` restricts to a whole-path equality (no descendant match).
|
|
692
|
+
function _skipStrMatch(path, entry, exact) {
|
|
693
|
+
if (exact) return path === entry;
|
|
694
|
+
if (entry.charAt(entry.length - 1) === "/") return path.indexOf(entry) === 0;
|
|
695
|
+
return path === entry || path.indexOf(entry + "/") === 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function makeSkipMatcher(opts, label) {
|
|
699
|
+
opts = opts || {};
|
|
700
|
+
label = label || "makeSkipMatcher";
|
|
701
|
+
var skipPaths = opts.skipPaths || [];
|
|
702
|
+
if (!Array.isArray(skipPaths)) {
|
|
703
|
+
throw new TypeError(label + ": skipPaths must be an array of string prefixes or RegExp");
|
|
704
|
+
}
|
|
705
|
+
for (var i = 0; i < skipPaths.length; i++) {
|
|
706
|
+
if (typeof skipPaths[i] !== "string" && !(skipPaths[i] instanceof RegExp)) {
|
|
707
|
+
throw new TypeError(label + ": skipPaths[" + i + "] must be a string prefix or RegExp, got " +
|
|
708
|
+
typeof skipPaths[i]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
var skipFn = opts.skip;
|
|
712
|
+
if (skipFn !== undefined && skipFn !== null && typeof skipFn !== "function") {
|
|
713
|
+
throw new TypeError(label + ": skip must be a function (req) => boolean");
|
|
714
|
+
}
|
|
715
|
+
var exact = opts.exact === true;
|
|
716
|
+
return function _shouldSkip(req) {
|
|
717
|
+
var path = (req && (req.pathname || req.url || req.originalUrl)) || "/";
|
|
718
|
+
var qpos = path.indexOf("?");
|
|
719
|
+
if (qpos !== -1) path = path.slice(0, qpos); // match on the path, never the query string
|
|
720
|
+
for (var j = 0; j < skipPaths.length; j++) {
|
|
721
|
+
var entry = skipPaths[j];
|
|
722
|
+
if (typeof entry === "string" ? _skipStrMatch(path, entry, exact) : entry.test(path)) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (skipFn) {
|
|
727
|
+
try { return skipFn(req) === true; }
|
|
728
|
+
catch (_e) { return false; }
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
471
734
|
/**
|
|
472
735
|
* @primitive b.requestHelpers.captureResponseStatus
|
|
473
736
|
* @signature b.requestHelpers.captureResponseStatus(res, onEnd)
|
|
@@ -667,11 +930,8 @@ function extractBearer(req) {
|
|
|
667
930
|
if (raw.indexOf(",") !== -1) return null;
|
|
668
931
|
// Reject ASCII control characters BEFORE prefix-matching so a header
|
|
669
932
|
// like "Bearer\rinjected" never reaches consumers.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (cc === 0x00 || cc === 0x0A || cc === 0x0D || cc === 0x09 || cc < 0x20 || cc === 0x7F) {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
933
|
+
if (codepointClass.firstControlCharOffset(raw, { forbidTab: true }) !== -1) {
|
|
934
|
+
return null;
|
|
675
935
|
}
|
|
676
936
|
// RFC 6750 §2.1 — auth-scheme is case-insensitive. The "Bearer "
|
|
677
937
|
// prefix + at least one token byte must be present; the literal
|
|
@@ -747,15 +1007,58 @@ function safeHeadersDistinct(req) {
|
|
|
747
1007
|
// skip __proto__ / constructor / prototype as keys — they are the
|
|
748
1008
|
// exact strings that triggered the upstream getter throw, and we
|
|
749
1009
|
// refuse to surface them as accessible header names.
|
|
750
|
-
if (lower
|
|
1010
|
+
if (pick.isPoisonedKey(lower)) continue;
|
|
751
1011
|
if (out[lower]) out[lower].push(value);
|
|
752
1012
|
else out[lower] = [value];
|
|
753
1013
|
}
|
|
754
1014
|
return out;
|
|
755
1015
|
}
|
|
756
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* @primitive b.requestHelpers.makeResourceAuditEmitter
|
|
1019
|
+
* @signature b.requestHelpers.makeResourceAuditEmitter(sink, resourceKind, idFor?)
|
|
1020
|
+
* @since 0.15.13
|
|
1021
|
+
* @status stable
|
|
1022
|
+
* @related b.requestHelpers.extractActorContext
|
|
1023
|
+
*
|
|
1024
|
+
* Build a drop-silent audit emitter `(action, key, outcome, metadata, req)` for
|
|
1025
|
+
* a request-scoped resource. The emitter is disabled when `sink` is falsy (the
|
|
1026
|
+
* operator supplied no audit instance), so a primitive can wire it
|
|
1027
|
+
* unconditionally and let the operator opt in by passing `opts.audit`. Each
|
|
1028
|
+
* event carries `resource: { kind, id }` and, when a request is passed, the
|
|
1029
|
+
* actor extracted from it (`extractActorContext`); a throwing sink is swallowed
|
|
1030
|
+
* so audit emission can never break the request the event describes.
|
|
1031
|
+
*
|
|
1032
|
+
* The auth lockout / bot-challenge and session device-binding primitives emit
|
|
1033
|
+
* this exact shape, varying only in the resource kind and how the id derives
|
|
1034
|
+
* from the per-call key. `idFor(key)` maps the per-call key to the resource id
|
|
1035
|
+
* (default: the key verbatim); pass it when the id needs a prefix or transform.
|
|
1036
|
+
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* var emitAudit = b.requestHelpers.makeResourceAuditEmitter(
|
|
1039
|
+
* opts.audit, "auth.lockout", function (key) { return ns + ":" + key; });
|
|
1040
|
+
* emitAudit("locked", key, "denied", { attempts: n }, req);
|
|
1041
|
+
*/
|
|
1042
|
+
function makeResourceAuditEmitter(sink, resourceKind, idFor) {
|
|
1043
|
+
return function (action, key, outcome, metadata, req) {
|
|
1044
|
+
if (!sink) return;
|
|
1045
|
+
try {
|
|
1046
|
+
var event = {
|
|
1047
|
+
action: action,
|
|
1048
|
+
outcome: outcome,
|
|
1049
|
+
resource: { kind: resourceKind, id: idFor ? idFor(key) : key },
|
|
1050
|
+
metadata: metadata || {},
|
|
1051
|
+
};
|
|
1052
|
+
if (req) event.actor = extractActorContext(req);
|
|
1053
|
+
sink.safeEmit(event);
|
|
1054
|
+
} catch (_e) { /* audit best-effort — never let a sink throw escape */ }
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
757
1058
|
module.exports = {
|
|
758
1059
|
resolveRoute: resolveRoute,
|
|
1060
|
+
makeResourceAuditEmitter: makeResourceAuditEmitter,
|
|
1061
|
+
makeSkipMatcher: makeSkipMatcher,
|
|
759
1062
|
captureResponseStatus: captureResponseStatus,
|
|
760
1063
|
extractActorContext: extractActorContext,
|
|
761
1064
|
resolveActorWithOverride: resolveActorWithOverride,
|
|
@@ -763,7 +1066,9 @@ module.exports = {
|
|
|
763
1066
|
parseListHeader: parseListHeader,
|
|
764
1067
|
// proxy-trust primitives (default refuses forwarded headers)
|
|
765
1068
|
clientIp: clientIp,
|
|
1069
|
+
trustedClientIp: trustedClientIp,
|
|
766
1070
|
requestProtocol: requestProtocol,
|
|
1071
|
+
trustedProtocol: trustedProtocol,
|
|
767
1072
|
appendVary: appendVary,
|
|
768
1073
|
// CVE-2026-21710 wrap — safe alternative to req.headersDistinct
|
|
769
1074
|
safeHeadersDistinct: safeHeadersDistinct,
|
|
@@ -49,7 +49,7 @@ function create(opts) {
|
|
|
49
49
|
validateOpts.requireNonEmptyString(opts.resource, "resource",
|
|
50
50
|
ResourceAccessLockError, "resource-access-lock/no-resource");
|
|
51
51
|
var startMode = opts.startMode || "open";
|
|
52
|
-
if (!VALID_MODES
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_MODES, startMode)) {
|
|
53
53
|
throw new ResourceAccessLockError(
|
|
54
54
|
"resource-access-lock/bad-start-mode",
|
|
55
55
|
"startMode must be one of: " + Object.keys(VALID_MODES).join(" / "));
|
|
@@ -71,12 +71,12 @@ function create(opts) {
|
|
|
71
71
|
function permits(action) {
|
|
72
72
|
if (mode === "open") return true;
|
|
73
73
|
if (mode === "locked") return false;
|
|
74
|
-
return !!READ_ACTIONS
|
|
74
|
+
return !!Object.prototype.hasOwnProperty.call(READ_ACTIONS, action);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function set(newMode, ctx) {
|
|
78
78
|
ctx = ctx || {};
|
|
79
|
-
if (!VALID_MODES
|
|
79
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_MODES, newMode)) {
|
|
80
80
|
throw new ResourceAccessLockError(
|
|
81
81
|
"resource-access-lock/bad-mode",
|
|
82
82
|
"set: mode must be one of: " + Object.keys(VALID_MODES).join(" / "));
|
package/lib/restore-bundle.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
var nodeFs = require("node:fs");
|
|
49
49
|
var nodePath = require("node:path");
|
|
50
50
|
var atomicFile = require("./atomic-file");
|
|
51
|
+
var C = require("./constants");
|
|
51
52
|
var backupCrypto = require("./backup/crypto");
|
|
52
53
|
var backupManifest = require("./backup/manifest");
|
|
53
54
|
var validateOpts = require("./validate-opts");
|
|
@@ -146,13 +147,21 @@ async function extract(opts) {
|
|
|
146
147
|
// 1. Read + parse + validate manifest
|
|
147
148
|
_emit(progress, { phase: "read_manifest" });
|
|
148
149
|
var manifestPath = nodePath.join(bundleDir, "manifest.json");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// Capped fd-bound read OUTSIDE the parse try/catch, so a missing / oversized
|
|
151
|
+
// bundle manifest surfaces the precise restore-bundle code (not a generic
|
|
152
|
+
// parse error). A backup manifest is small JSON; 4 MiB bounds a hostile one
|
|
153
|
+
// before backupManifest.parse materializes it.
|
|
154
|
+
var manifestRaw = atomicFile.fdSafeReadSync(manifestPath, {
|
|
155
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
156
|
+
errorFor: function (kind, detail) {
|
|
157
|
+
if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-manifest", "extract: bundleDir has no manifest.json — bundle is incomplete or not a blamejs backup");
|
|
158
|
+
if (kind === "too-large") return new RestoreBundleError("restore-bundle/bad-manifest", "extract: manifest.json exceeds " + detail.max + " bytes");
|
|
159
|
+
return new RestoreBundleError("restore-bundle/bad-manifest", "extract: manifest unreadable: " + kind);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
153
162
|
var manifest;
|
|
154
163
|
try {
|
|
155
|
-
manifest = backupManifest.parse(
|
|
164
|
+
manifest = backupManifest.parse(manifestRaw);
|
|
156
165
|
} catch (e) {
|
|
157
166
|
if (e && e.isBackupManifestError) throw e;
|
|
158
167
|
throw new RestoreBundleError("restore-bundle/bad-manifest",
|
|
@@ -216,12 +225,20 @@ async function extract(opts) {
|
|
|
216
225
|
}
|
|
217
226
|
|
|
218
227
|
var blobPath = nodePath.join(bundleDir, entry.encryptedPath);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
// Cap the read to the manifest's declared encryptedSize so an oversize-on-
|
|
229
|
+
// disk blob is refused BEFORE it is read into memory (was: read fully, then
|
|
230
|
+
// compare → an OOM window for a huge swapped blob). A valid blob is exactly
|
|
231
|
+
// encryptedSize; the post-read compare still catches the under-size case.
|
|
232
|
+
var blobCap = (typeof entry.encryptedSize === "number" && entry.encryptedSize > 0)
|
|
233
|
+
? entry.encryptedSize : C.BYTES.gib(8);
|
|
234
|
+
var blob = atomicFile.fdSafeReadSync(blobPath, {
|
|
235
|
+
maxBytes: blobCap,
|
|
236
|
+
errorFor: function (kind, detail) {
|
|
237
|
+
if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-blob", "extract: manifest references '" + entry.encryptedPath + "' but the bundle has no such file");
|
|
238
|
+
if (kind === "too-large") return new RestoreBundleError("restore-bundle/size-mismatch", "extract: blob '" + entry.encryptedPath + "' has size " + detail.size + " but manifest expected " + entry.encryptedSize);
|
|
239
|
+
return new RestoreBundleError("restore-bundle/missing-blob", "extract: blob '" + entry.encryptedPath + "' unreadable: " + kind);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
225
242
|
if (blob.length !== entry.encryptedSize) {
|
|
226
243
|
throw new RestoreBundleError("restore-bundle/size-mismatch",
|
|
227
244
|
"extract: blob '" + entry.encryptedPath + "' has size " + blob.length +
|
|
@@ -235,12 +252,18 @@ async function extract(opts) {
|
|
|
235
252
|
|
|
236
253
|
var plaintext;
|
|
237
254
|
try {
|
|
238
|
-
|
|
255
|
+
// Bundles written with manifest.aadBound sealed each blob with its
|
|
256
|
+
// relativePath as AEAD associated data — pass the SAME path so a blob
|
|
257
|
+
// remapped to a different manifest entry fails the tag here (the
|
|
258
|
+
// blob-remap / restore-corruption defense). Legacy bundles without
|
|
259
|
+
// the flag decrypt with no AAD (backward compatible).
|
|
260
|
+
var blobAad = manifest.aadBound === true ? Buffer.from(entry.relativePath, "utf8") : undefined;
|
|
261
|
+
plaintext = await backupCrypto.decryptWithPassphrase(blob, passphrase, entry.salt, blobAad);
|
|
239
262
|
} catch (e) {
|
|
240
263
|
if (e && e.isBackupCryptoError && e.code === "backup-crypto/decrypt-failed") {
|
|
241
264
|
throw new RestoreBundleError("restore-bundle/decrypt-failed",
|
|
242
265
|
"extract: blob '" + entry.encryptedPath + "' did not decrypt — " +
|
|
243
|
-
"passphrase rejected or
|
|
266
|
+
"passphrase rejected, ciphertext tampered, or blob remapped to a different path");
|
|
244
267
|
}
|
|
245
268
|
throw e;
|
|
246
269
|
}
|
|
@@ -326,11 +349,16 @@ function inspect(opts) {
|
|
|
326
349
|
"inspect: opts.bundleDir is required and must exist");
|
|
327
350
|
}
|
|
328
351
|
var manifestPath = nodePath.join(opts.bundleDir, "manifest.json");
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
// Capped fd-bound read (no-passphrase preview path, reachable with only read
|
|
353
|
+
// access to the bundle): bound a hostile manifest before parse.
|
|
354
|
+
return backupManifest.parse(atomicFile.fdSafeReadSync(manifestPath, {
|
|
355
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
356
|
+
errorFor: function (kind) {
|
|
357
|
+
if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-manifest", "inspect: bundleDir has no manifest.json");
|
|
358
|
+
if (kind === "too-large") return new RestoreBundleError("restore-bundle/bad-manifest", "inspect: manifest.json too large");
|
|
359
|
+
return new RestoreBundleError("restore-bundle/missing-manifest", "inspect: manifest unreadable: " + kind);
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
334
362
|
}
|
|
335
363
|
|
|
336
364
|
module.exports = {
|