@blamejs/core 0.15.13 → 0.15.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +38 -6
- package/lib/agent-event-bus.js +13 -0
- package/lib/agent-idempotency.js +5 -1
- package/lib/agent-snapshot.js +32 -2
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +9 -7
- package/lib/archive-tar-read.js +51 -8
- package/lib/archive.js +4 -2
- package/lib/asn1-der.js +70 -22
- package/lib/atomic-file.js +204 -2
- package/lib/audit-chain.js +54 -5
- package/lib/audit-daily-review.js +12 -2
- package/lib/audit-sign.js +2 -1
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +7 -2
- package/lib/auth/access-lock.js +2 -2
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +71 -8
- package/lib/auth/dpop.js +15 -1
- package/lib/auth/fido-mds3.js +30 -13
- package/lib/auth/jwt.js +21 -5
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/oauth.js +8 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +42 -12
- package/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/auth/status-list.js +14 -2
- package/lib/auth/step-up.js +9 -1
- package/lib/auth-bot-challenge.js +21 -2
- package/lib/backup/bundle.js +7 -2
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +41 -20
- package/lib/backup/manifest.js +7 -1
- package/lib/break-glass.js +41 -22
- package/lib/cbor.js +34 -11
- package/lib/cdn-cache-control.js +7 -3
- package/lib/cert.js +5 -3
- package/lib/cli.js +5 -1
- package/lib/cloud-events.js +3 -2
- package/lib/cluster-storage.js +7 -3
- package/lib/codepoint-class.js +17 -0
- package/lib/compliance-eaa.js +1 -1
- package/lib/compliance-sanctions.js +9 -7
- package/lib/compliance.js +1 -1
- package/lib/config-drift.js +22 -8
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +11 -4
- package/lib/cookies.js +10 -2
- package/lib/cose.js +20 -0
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +48 -24
- package/lib/crypto.js +18 -4
- package/lib/csp.js +13 -0
- package/lib/daemon.js +4 -1
- package/lib/data-act.js +27 -4
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +102 -11
- package/lib/db.js +32 -15
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +22 -8
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db.js +60 -4
- package/lib/fda-21cfr11.js +30 -5
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/gate-contract.js +46 -5
- package/lib/gdpr-ropa.js +18 -9
- package/lib/graphql-federation.js +17 -4
- package/lib/guard-all.js +2 -2
- package/lib/guard-dsn.js +1 -1
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-html.js +9 -11
- package/lib/guard-imap-command.js +1 -1
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +14 -6
- package/lib/guard-mail-move.js +1 -1
- package/lib/guard-managesieve-command.js +1 -1
- package/lib/guard-pop3-command.js +1 -1
- package/lib/guard-smtp-command.js +1 -1
- package/lib/guard-svg.js +8 -9
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cookie-jar.js +33 -12
- package/lib/http-client.js +225 -53
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/incident-report.js +9 -6
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/legal-hold.js +24 -8
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arf.js +1 -1
- package/lib/mail-auth.js +27 -4
- package/lib/mail-bimi.js +16 -16
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-deploy.js +9 -5
- package/lib/mail-dkim.js +20 -7
- package/lib/mail-greylist.js +2 -4
- package/lib/mail-helo.js +2 -4
- package/lib/mail-journal.js +11 -8
- package/lib/mail-mdn.js +8 -4
- package/lib/mail-rbl.js +2 -4
- package/lib/mail-scan.js +3 -5
- package/lib/mail-server-jmap.js +4 -1
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-tls.js +9 -2
- package/lib/mail-spam-score.js +2 -4
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail.js +22 -2
- package/lib/markup-tokenizer.js +24 -0
- package/lib/mcp.js +6 -4
- package/lib/mdoc.js +26 -3
- package/lib/metrics.js +14 -3
- package/lib/middleware/api-encrypt.js +2 -2
- package/lib/middleware/body-parser.js +10 -4
- package/lib/middleware/bot-guard.js +26 -18
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compose-pipeline.js +39 -5
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +60 -21
- package/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/middleware/fetch-metadata.js +28 -4
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +25 -16
- package/lib/middleware/scim-server.js +2 -1
- package/lib/middleware/security-headers.js +24 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +2 -2
- package/lib/money.js +1 -1
- package/lib/mtls-ca.js +10 -6
- package/lib/network-dns-resolver.js +1 -1
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +2 -1
- package/lib/network-smtp-policy.js +23 -5
- package/lib/network-tls.js +27 -5
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +1 -1
- package/lib/nist-crosswalk.js +1 -1
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +9 -0
- package/lib/object-store/azure-blob.js +1 -2
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +6 -4
- package/lib/object-store/http-put.js +1 -2
- package/lib/object-store/http-request.js +30 -1
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4.js +1 -2
- package/lib/observability-otlp-exporter.js +20 -4
- package/lib/outbox.js +11 -4
- package/lib/parsers/safe-xml.js +1 -1
- package/lib/parsers/safe-yaml.js +21 -3
- package/lib/pipl-cn.js +11 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +347 -36
- 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 +19 -0
- package/lib/retention.js +20 -4
- package/lib/router.js +17 -4
- package/lib/safe-ical.js +2 -2
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +70 -0
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-vcard.js +1 -1
- package/lib/sandbox-worker.js +6 -0
- package/lib/sandbox.js +1 -1
- package/lib/scheduler.js +17 -1
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/session.js +62 -120
- package/lib/sql.js +25 -3
- package/lib/static.js +65 -13
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +52 -19
- package/lib/tsa.js +5 -2
- 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 +4 -1
- package/lib/vc.js +1 -1
- 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/webhook.js +16 -1
- package/lib/websocket.js +1 -1
- package/lib/worm.js +1 -1
- package/lib/ws-client.js +83 -46
- package/lib/x509-chain.js +91 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/parsers/safe-yaml.js
CHANGED
|
@@ -771,9 +771,27 @@ function parse(input, opts) {
|
|
|
771
771
|
}
|
|
772
772
|
|
|
773
773
|
function _stripEolComment(text) {
|
|
774
|
-
// Strip ` #...` comments (must be preceded by
|
|
775
|
-
|
|
776
|
-
|
|
774
|
+
// Strip ` #...` comments (a YAML end-of-line comment must be preceded by
|
|
775
|
+
// whitespace) via a single linear scan. The previous /^(.*?)(\s+#.*)?$/
|
|
776
|
+
// backtracks polynomially on a long inline-whitespace run with no '#'
|
|
777
|
+
// (each position re-tries `\s+#`) — a ReDoS lever on attacker-supplied
|
|
778
|
+
// YAML. This finds the first whitespace-preceded '#' and cuts the
|
|
779
|
+
// preceding whitespace run, identical output without the catastrophic
|
|
780
|
+
// backtracking.
|
|
781
|
+
for (var i = 1; i < text.length; i++) {
|
|
782
|
+
if (text.charCodeAt(i) !== 0x23 /* # */) continue;
|
|
783
|
+
var prev = text.charCodeAt(i - 1);
|
|
784
|
+
// inline whitespace: space / tab / vtab / formfeed / CR
|
|
785
|
+
if (prev !== 0x20 && prev !== 0x09 && prev !== 0x0b && prev !== 0x0c && prev !== 0x0d) continue;
|
|
786
|
+
var end = i;
|
|
787
|
+
while (end > 0) {
|
|
788
|
+
var c = text.charCodeAt(end - 1);
|
|
789
|
+
if (c === 0x20 || c === 0x09 || c === 0x0b || c === 0x0c || c === 0x0d) end--;
|
|
790
|
+
else break;
|
|
791
|
+
}
|
|
792
|
+
return safeBuffer.stripTrailingHspace(text.slice(0, end));
|
|
793
|
+
}
|
|
794
|
+
return safeBuffer.stripTrailingHspace(text);
|
|
777
795
|
}
|
|
778
796
|
|
|
779
797
|
// ---- Block scalars (| literal, > folded) ----
|
package/lib/pipl-cn.js
CHANGED
|
@@ -108,14 +108,17 @@ function _requireRecordedAt(value, label) {
|
|
|
108
108
|
* and determine the lawful mechanism the transfer requires. PIPL Art. 38(1)
|
|
109
109
|
* permits three bases for moving personal information out of the PRC — the
|
|
110
110
|
* CAC standard contract (SCC), a CAC security assessment (Art. 40), or
|
|
111
|
-
* certification by a CAC-accredited body
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* operator (CIIO),
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* individuals
|
|
111
|
+
* certification by a CAC-accredited body. Under the CAC Provisions on
|
|
112
|
+
* Promoting and Regulating Cross-Border Data Flows (effective 2024, which
|
|
113
|
+
* relaxed the 2022 thresholds) the security assessment is MANDATORY (the
|
|
114
|
+
* operator may NOT self-select the standard contract or certification) when
|
|
115
|
+
* the exporter is a critical-information-infrastructure operator (CIIO),
|
|
116
|
+
* exports "important data", or — counting cumulatively since 1 January of
|
|
117
|
+
* the current year — transfers the personal information of more than
|
|
118
|
+
* 1,000,000 individuals (non-sensitive) or the sensitive personal
|
|
119
|
+
* information of more than 10,000 individuals. The 100,000–1,000,000
|
|
120
|
+
* non-sensitive band is the standard-contract / certification tier, NOT a
|
|
121
|
+
* security-assessment trigger.
|
|
119
122
|
*
|
|
120
123
|
* The builder validates the operator-supplied facts, computes
|
|
121
124
|
* `securityAssessmentRequired` against those thresholds, resolves the
|
package/lib/queue-local.js
CHANGED
|
@@ -406,7 +406,13 @@ function create(config) {
|
|
|
406
406
|
.where("_id", jobId)
|
|
407
407
|
.where("status", "inflight")
|
|
408
408
|
.toSql();
|
|
409
|
-
await store.execute(doneBuilt.sql, doneBuilt.params);
|
|
409
|
+
var doneRes = await store.execute(doneBuilt.sql, doneBuilt.params);
|
|
410
|
+
// Only run the post-completion side effects if THIS call actually flipped
|
|
411
|
+
// the row inflight→done. If the status guard matched 0 rows the job was
|
|
412
|
+
// already completed or re-leased to another worker (its lease expired and
|
|
413
|
+
// sweepExpired re-queued it) — a stale completer must NOT re-enqueue the
|
|
414
|
+
// cron repeat (duplicate firing) or release flow children twice.
|
|
415
|
+
if ((doneRes.rowCount || 0) === 0) return false;
|
|
410
416
|
|
|
411
417
|
// Repeat-in-queue: cron-recurring job re-enqueues itself for the
|
|
412
418
|
// next firing time. Failures (which take the fail() path) don't
|
|
@@ -513,9 +519,10 @@ function create(config) {
|
|
|
513
519
|
[nowMs + retryDelayMs])
|
|
514
520
|
.setRaw("finishedAt", "CASE WHEN " + attemptsLt + " THEN NULL ELSE ? END", [nowMs])
|
|
515
521
|
.where("_id", jobId)
|
|
522
|
+
.where("status", "inflight") // lease-ownership guard — a stale fail() must not clobber a job re-leased to another worker after this one's lease expired (mirrors complete()/extendLease)
|
|
516
523
|
.toSql();
|
|
517
|
-
await store.execute(failBuilt.sql, failBuilt.params);
|
|
518
|
-
return
|
|
524
|
+
var failRes = await store.execute(failBuilt.sql, failBuilt.params);
|
|
525
|
+
return (failRes.rowCount || 0) > 0;
|
|
519
526
|
}
|
|
520
527
|
|
|
521
528
|
async function sweepExpired() {
|
package/lib/redact.js
CHANGED
|
@@ -278,12 +278,16 @@ function redact(value, opts) {
|
|
|
278
278
|
function _redact(value, depth, maxDepth, marker, parentKey) {
|
|
279
279
|
if (depth > maxDepth) return marker;
|
|
280
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;
|
|
281
287
|
if (typeof value === "string") {
|
|
282
|
-
if (parentKey && _isSensitiveFieldName(parentKey)) return marker;
|
|
283
288
|
return _redactValue(value);
|
|
284
289
|
}
|
|
285
290
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
286
|
-
if (parentKey && _isSensitiveFieldName(parentKey)) return marker;
|
|
287
291
|
return value;
|
|
288
292
|
}
|
|
289
293
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
@@ -513,7 +517,7 @@ function classifyDefaults(opts) {
|
|
|
513
517
|
"redact.classifyDefaults: patterns[" + p + "] must be a string, got " +
|
|
514
518
|
typeof patterns[p]);
|
|
515
519
|
}
|
|
516
|
-
if (!CLASSIFIER_PATTERNS
|
|
520
|
+
if (!Object.prototype.hasOwnProperty.call(CLASSIFIER_PATTERNS, patterns[p]) &&
|
|
517
521
|
!(opts.extra && opts.extra[patterns[p]])) {
|
|
518
522
|
throw new DlpError("redact-dlp/unknown-pattern",
|
|
519
523
|
"redact.classifyDefaults: unknown pattern '" + patterns[p] +
|
package/lib/request-helpers.js
CHANGED
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
var structuredFields = require("./structured-fields");
|
|
45
45
|
var pick = require("./pick");
|
|
46
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"); });
|
|
47
52
|
|
|
48
53
|
var HTTP_STATUS = Object.freeze({
|
|
49
54
|
OK: 0xC8,
|
|
@@ -214,15 +219,24 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
|
214
219
|
*
|
|
215
220
|
* Resolve the originating client IP from a request. Default reads
|
|
216
221
|
* only `req.socket.remoteAddress` — `X-Forwarded-For` is ignored
|
|
217
|
-
* because without a sanitizing reverse proxy it's
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* `trustProxy
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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.
|
|
223
237
|
*
|
|
224
238
|
* @opts
|
|
225
|
-
* trustProxy: boolean | number // false (default) |
|
|
239
|
+
* trustProxy: boolean | number | function // false (default) | predicate (peer-gated) | legacy true/hop-count
|
|
226
240
|
*
|
|
227
241
|
* @example
|
|
228
242
|
* var req = {
|
|
@@ -232,30 +246,55 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
|
232
246
|
* b.requestHelpers.clientIp(req);
|
|
233
247
|
* // → "10.0.0.1" (forwarded headers ignored by default)
|
|
234
248
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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)
|
|
237
252
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
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)
|
|
240
257
|
*
|
|
241
258
|
* b.requestHelpers.clientIp(undefined);
|
|
242
259
|
* // → null
|
|
243
260
|
*/
|
|
244
261
|
function clientIp(req, opts) {
|
|
245
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;
|
|
246
267
|
var trust = opts && opts.trustProxy;
|
|
247
268
|
if (trust && req.headers) {
|
|
248
269
|
var xff = req.headers["x-forwarded-for"];
|
|
249
270
|
if (xff) {
|
|
250
271
|
var hops = parseListHeader(xff);
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|
|
254
294
|
}
|
|
255
295
|
}
|
|
256
296
|
}
|
|
257
|
-
if (
|
|
258
|
-
if (req.connection && typeof req.connection.remoteAddress === "string") return req.connection.remoteAddress;
|
|
297
|
+
if (socketAddr) return socketAddr;
|
|
259
298
|
// Express-shaped requests expose the resolved client address as `req.ip`
|
|
260
299
|
// (Express derives it from the socket, honoring its own trust-proxy
|
|
261
300
|
// setting) without a `socket.remoteAddress` surface. Fall back to it so a
|
|
@@ -266,6 +305,253 @@ function clientIp(req, opts) {
|
|
|
266
305
|
return null;
|
|
267
306
|
}
|
|
268
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
|
+
// IP-prefix masking constants — named so the bit-arithmetic stays readable.
|
|
388
|
+
// /24 IPv4 is the original IP-geolocation bucket and matches the legacy
|
|
389
|
+
// carrier-NAT pool stride; /64 IPv6 is the customer LAN every RIR allocates
|
|
390
|
+
// (RFC 4291 §2.5.4), so tightening below it punishes IPv6 privacy-extension
|
|
391
|
+
// address rotation.
|
|
392
|
+
var IP_BITS_PER_BYTE = 8; // bits per byte; protocol constant, not a byte size
|
|
393
|
+
var IPV4_OCTET_COUNT = 4;
|
|
394
|
+
var IPV4_OCTET_RANGE = 256; // 0..255 inclusive; v4 octet domain
|
|
395
|
+
var IPV4_TOTAL_BITS = 32; // IPv4 address width in bits
|
|
396
|
+
var IPV4_DEFAULT_PREFIX = 24; // /24 carrier-NAT pool stride
|
|
397
|
+
var IPV6_GROUP_COUNT = 8; // 8 16-bit groups in v6
|
|
398
|
+
var IPV6_BYTE_COUNT = 16; // 16 bytes in v6
|
|
399
|
+
var IPV6_DEFAULT_PREFIX = 64; // /64 customer LAN per RFC 4291 §2.5.4
|
|
400
|
+
var IP_BYTE_MASK = 0xff;
|
|
401
|
+
var IP_HEX_RADIX = 16; // base-16 radix
|
|
402
|
+
var V4_MAPPED_V6_PREFIX = "::ffff:";
|
|
403
|
+
|
|
404
|
+
function _maskIpv4(ip, prefix) {
|
|
405
|
+
// ip = "a.b.c.d"; prefix is bits to keep (1..32).
|
|
406
|
+
var parts = String(ip).split(".");
|
|
407
|
+
if (parts.length !== IPV4_OCTET_COUNT) return null;
|
|
408
|
+
var n = 0;
|
|
409
|
+
for (var i = 0; i < IPV4_OCTET_COUNT; i++) {
|
|
410
|
+
var oct = parseInt(parts[i], 10);
|
|
411
|
+
if (!Number.isInteger(oct) || oct < 0 || oct >= IPV4_OCTET_RANGE) return null;
|
|
412
|
+
n = (n * IPV4_OCTET_RANGE) + oct;
|
|
413
|
+
}
|
|
414
|
+
// Apply prefix mask.
|
|
415
|
+
var mask = prefix === 0 ? 0 : (-1 >>> (IPV4_TOTAL_BITS - prefix)) << (IPV4_TOTAL_BITS - prefix);
|
|
416
|
+
// Bitwise on 32-bit unsigned. JS coerces to 32-bit signed, so use
|
|
417
|
+
// unsigned right shift to recover.
|
|
418
|
+
var masked = (n & mask) >>> 0;
|
|
419
|
+
return ((masked >>> IP_BITS_PER_BYTE * 3) & IP_BYTE_MASK) + "." +
|
|
420
|
+
((masked >>> IP_BITS_PER_BYTE * 2) & IP_BYTE_MASK) + "." +
|
|
421
|
+
((masked >>> IP_BITS_PER_BYTE) & IP_BYTE_MASK) + "." +
|
|
422
|
+
(masked & IP_BYTE_MASK) + "/" + prefix;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function _maskIpv6(ip, prefix) {
|
|
426
|
+
// Expand to 8 16-bit groups. Accept :: shorthand. Reject if invalid.
|
|
427
|
+
var raw = String(ip).toLowerCase();
|
|
428
|
+
// Strip an embedded zone id (fe80::1%eth0); not part of the address.
|
|
429
|
+
var pct = raw.indexOf("%");
|
|
430
|
+
if (pct !== -1) raw = raw.substring(0, pct);
|
|
431
|
+
var doubleColonAt = raw.indexOf("::");
|
|
432
|
+
var groups;
|
|
433
|
+
if (doubleColonAt === -1) {
|
|
434
|
+
groups = raw.split(":");
|
|
435
|
+
if (groups.length !== IPV6_GROUP_COUNT) return null;
|
|
436
|
+
} else {
|
|
437
|
+
var left = raw.substring(0, doubleColonAt).split(":");
|
|
438
|
+
var right = raw.substring(doubleColonAt + 2).split(":");
|
|
439
|
+
if (left.length === 1 && left[0] === "") left = [];
|
|
440
|
+
if (right.length === 1 && right[0] === "") right = [];
|
|
441
|
+
var fillCount = IPV6_GROUP_COUNT - left.length - right.length;
|
|
442
|
+
if (fillCount < 0) return null;
|
|
443
|
+
var middle = [];
|
|
444
|
+
for (var fi = 0; fi < fillCount; fi++) middle.push("0");
|
|
445
|
+
groups = left.concat(middle).concat(right);
|
|
446
|
+
}
|
|
447
|
+
// Each group is 1–4 hex chars.
|
|
448
|
+
var bytes = [];
|
|
449
|
+
for (var gi = 0; gi < IPV6_GROUP_COUNT; gi++) {
|
|
450
|
+
var g = groups[gi];
|
|
451
|
+
if (typeof g !== "string" || g.length === 0 || g.length > 4 || /[^0-9a-f]/.test(g)) return null;
|
|
452
|
+
var v = parseInt(g, IP_HEX_RADIX);
|
|
453
|
+
if (!Number.isInteger(v) || v < 0 || v > 0xffff) return null;
|
|
454
|
+
bytes.push((v >> IP_BITS_PER_BYTE) & IP_BYTE_MASK);
|
|
455
|
+
bytes.push(v & IP_BYTE_MASK);
|
|
456
|
+
}
|
|
457
|
+
// Apply prefix in bits.
|
|
458
|
+
var keepBytes = Math.floor(prefix / IP_BITS_PER_BYTE);
|
|
459
|
+
var keepBits = prefix % IP_BITS_PER_BYTE;
|
|
460
|
+
for (var bi = 0; bi < IPV6_BYTE_COUNT; bi++) {
|
|
461
|
+
if (bi < keepBytes) continue;
|
|
462
|
+
if (bi === keepBytes && keepBits > 0) {
|
|
463
|
+
var m = (IP_BYTE_MASK << (IP_BITS_PER_BYTE - keepBits)) & IP_BYTE_MASK;
|
|
464
|
+
bytes[bi] = bytes[bi] & m;
|
|
465
|
+
} else {
|
|
466
|
+
bytes[bi] = 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Re-emit as colon-hex (no compression — deterministic for hashing).
|
|
470
|
+
var out = [];
|
|
471
|
+
for (var oi = 0; oi < IPV6_BYTE_COUNT; oi += 2) {
|
|
472
|
+
out.push(((bytes[oi] << IP_BITS_PER_BYTE) | bytes[oi + 1]).toString(IP_HEX_RADIX));
|
|
473
|
+
}
|
|
474
|
+
return out.join(":") + "/" + prefix;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @primitive b.requestHelpers.ipPrefix
|
|
479
|
+
* @signature b.requestHelpers.ipPrefix(ip)
|
|
480
|
+
* @since 0.15.15
|
|
481
|
+
* @related b.requestHelpers.clientIp, b.requestHelpers.trustedClientIp
|
|
482
|
+
*
|
|
483
|
+
* Mask a client IP to its subnet bucket: a <code>/24</code> for IPv4 (the
|
|
484
|
+
* carrier-NAT pool stride) and a <code>/64</code> for IPv6 (the customer-LAN
|
|
485
|
+
* prefix RIRs allocate, RFC 4291 §2.5.4). Returns the canonical
|
|
486
|
+
* <code>"network/prefix"</code> string, or <code>""</code> for a non-string /
|
|
487
|
+
* empty / unparseable input. An IPv4-mapped IPv6 address
|
|
488
|
+
* (<code>::ffff:1.2.3.4</code>) folds to its dotted form so it buckets the
|
|
489
|
+
* same regardless of how a proxy reported it.
|
|
490
|
+
*
|
|
491
|
+
* This is the masking the session device-fingerprint's built-in
|
|
492
|
+
* <code>clientIpPrefix</code> field hashes (so roaming carriers that flip the
|
|
493
|
+
* public IP within a subnet don't log a user out). Exposed so an operator who
|
|
494
|
+
* drops to a function-form fingerprint field — for a custom mask width, or to
|
|
495
|
+
* combine the prefix with other signals — reuses this exact algorithm instead
|
|
496
|
+
* of re-deriving the /24 + /64 masking (and silently diverging).
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* b.requestHelpers.ipPrefix("203.0.113.47"); // → "203.0.113.0/24"
|
|
500
|
+
* b.requestHelpers.ipPrefix("2001:db8::1"); // → "2001:db8:0:0/64"
|
|
501
|
+
*/
|
|
502
|
+
function ipPrefix(ip) {
|
|
503
|
+
if (typeof ip !== "string" || ip.length === 0) return "";
|
|
504
|
+
// IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4 mask
|
|
505
|
+
// applies. Same bucket regardless of how the proxy reported it.
|
|
506
|
+
var lower = ip.toLowerCase();
|
|
507
|
+
if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
|
|
508
|
+
return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), IPV4_DEFAULT_PREFIX) || "";
|
|
509
|
+
}
|
|
510
|
+
if (ip.indexOf(":") !== -1) return _maskIpv6(ip, IPV6_DEFAULT_PREFIX) || "";
|
|
511
|
+
if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
|
|
512
|
+
return "";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @primitive b.requestHelpers.trustedProtocol
|
|
517
|
+
* @signature b.requestHelpers.trustedProtocol(opts?)
|
|
518
|
+
* @since 0.15.14
|
|
519
|
+
* @related b.requestHelpers.requestProtocol, b.requestHelpers.trustedClientIp
|
|
520
|
+
*
|
|
521
|
+
* Peer-gated companion to trustedClientIp for the request scheme. The
|
|
522
|
+
* Secure-cookie / HSTS / secure-context decisions hinge on whether a request
|
|
523
|
+
* arrived over HTTPS; behind a TLS-terminating proxy that comes from
|
|
524
|
+
* X-Forwarded-Proto, which is forgeable unless the immediate peer is a trusted
|
|
525
|
+
* proxy. Returns `{ resolve(req)=>"http"|"https", peerGated }`. With
|
|
526
|
+
* `trustedProxies` (CIDRs) the header is honored only from a trusted peer; with
|
|
527
|
+
* `protocolResolver(req)` the operator owns the decision; with neither only the
|
|
528
|
+
* real TLS socket is consulted (forwarded headers ignored).
|
|
529
|
+
*
|
|
530
|
+
* @opts
|
|
531
|
+
* trustedProxies: string | string[],
|
|
532
|
+
* protocolResolver: function(req): "http"|"https",
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* var tp = b.requestHelpers.trustedProtocol({ trustedProxies: ["10.0.0.0/8"] });
|
|
536
|
+
* tp.resolve(req); // "https" only when X-Forwarded-Proto came via a trusted peer
|
|
537
|
+
*/
|
|
538
|
+
function trustedProtocol(opts) {
|
|
539
|
+
opts = opts || {};
|
|
540
|
+
var resolver = opts.protocolResolver;
|
|
541
|
+
if (resolver != null && typeof resolver !== "function") {
|
|
542
|
+
throw new TypeError("trustedProtocol: protocolResolver must be a function(req) => 'http'|'https'");
|
|
543
|
+
}
|
|
544
|
+
var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedProtocol");
|
|
545
|
+
return {
|
|
546
|
+
peerGated: !!(resolver || predicate),
|
|
547
|
+
resolve: function (req) {
|
|
548
|
+
if (resolver) return resolver(req);
|
|
549
|
+
if (predicate) return requestProtocol(req, { trustProxy: predicate });
|
|
550
|
+
return requestProtocol(req, { trustProxy: false });
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
269
555
|
/**
|
|
270
556
|
* @primitive b.requestHelpers.requestProtocol
|
|
271
557
|
* @signature b.requestHelpers.requestProtocol(req, opts?)
|
|
@@ -274,14 +560,17 @@ function clientIp(req, opts) {
|
|
|
274
560
|
*
|
|
275
561
|
* Resolve the inbound transport scheme. Default returns `"https"`
|
|
276
562
|
* when `req.socket.encrypted` is set, otherwise `"http"`. Behind a
|
|
277
|
-
* trusted reverse proxy that terminates TLS,
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
563
|
+
* trusted reverse proxy that terminates TLS, pass `trustProxy` as a
|
|
564
|
+
* PREDICATE `function(addr)=>boolean` naming your proxies:
|
|
565
|
+
* `X-Forwarded-Proto` is then honored only when the immediate peer is
|
|
566
|
+
* a trusted proxy, so a direct caller can't forge it (use
|
|
567
|
+
* `b.requestHelpers.trustedProtocol` to build this). The legacy
|
|
568
|
+
* `trustProxy: true` reads the leftmost hop without checking the peer —
|
|
569
|
+
* forgeable, safe only behind an edge that rewrites the header. Always
|
|
570
|
+
* returns a string; on bad input falls back to `"http"`.
|
|
282
571
|
*
|
|
283
572
|
* @opts
|
|
284
|
-
* trustProxy: boolean // false (default) | true
|
|
573
|
+
* trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true
|
|
285
574
|
*
|
|
286
575
|
* @example
|
|
287
576
|
* var req = { socket: { encrypted: true } };
|
|
@@ -305,7 +594,22 @@ function requestProtocol(req, opts) {
|
|
|
305
594
|
var fwd = req.headers["x-forwarded-proto"];
|
|
306
595
|
if (typeof fwd === "string" && fwd.length > 0) {
|
|
307
596
|
var hops = parseListHeader(fwd, { lowercase: true });
|
|
308
|
-
if (hops.length > 0)
|
|
597
|
+
if (hops.length > 0) {
|
|
598
|
+
if (typeof trust === "function") {
|
|
599
|
+
// Peer-gated: honor X-Forwarded-Proto only when the immediate TCP
|
|
600
|
+
// peer is a trusted proxy. A direct caller's forged header is
|
|
601
|
+
// ignored — fall through to the real TLS socket. The only form safe
|
|
602
|
+
// for a Secure-cookie / HSTS / secure-context decision.
|
|
603
|
+
var peer =
|
|
604
|
+
(req.socket && typeof req.socket.remoteAddress === "string" && req.socket.remoteAddress) ? req.socket.remoteAddress
|
|
605
|
+
: (req.connection && typeof req.connection.remoteAddress === "string" && req.connection.remoteAddress) ? req.connection.remoteAddress
|
|
606
|
+
: null;
|
|
607
|
+
if (peer && trust(peer)) return hops[0];
|
|
608
|
+
// peer not a trusted proxy → ignore forgeable header, fall through
|
|
609
|
+
} else {
|
|
610
|
+
return hops[0]; // legacy true/number — spoofable, see docstring
|
|
611
|
+
}
|
|
612
|
+
}
|
|
309
613
|
}
|
|
310
614
|
}
|
|
311
615
|
if (req.socket && req.socket.encrypted) return "https";
|
|
@@ -611,8 +915,6 @@ function captureResponseStatus(res, onEnd) {
|
|
|
611
915
|
return origEnd;
|
|
612
916
|
}
|
|
613
917
|
|
|
614
|
-
var Q_VALUE_RE = /(?:^|;|\s)q\s*=\s*([0-9]*\.?[0-9]+)/i;
|
|
615
|
-
|
|
616
918
|
/**
|
|
617
919
|
* @primitive b.requestHelpers.parseQualityList
|
|
618
920
|
* @signature b.requestHelpers.parseQualityList(headerValue, opts?)
|
|
@@ -655,24 +957,30 @@ function parseQualityList(headerValue, opts) {
|
|
|
655
957
|
if (typeof headerValue !== "string" || headerValue.length === 0) return [];
|
|
656
958
|
opts = opts || {};
|
|
657
959
|
var caseSensitive = opts.caseSensitive === true;
|
|
658
|
-
|
|
960
|
+
// Quote-aware split: a parameter value may be a quoted-string containing ','
|
|
961
|
+
// or ';' or 'q=' (RFC 7231 §5.3.1 / RFC 9110), which must not split an element
|
|
962
|
+
// or be mis-read as the q-value. splitUnquoted respects double-quoted runs.
|
|
963
|
+
var parts = structuredFields.splitUnquoted(headerValue, ",");
|
|
659
964
|
var out = [];
|
|
660
965
|
for (var i = 0; i < parts.length; i++) {
|
|
661
966
|
var p = parts[i].trim();
|
|
662
967
|
if (p.length === 0) continue;
|
|
663
|
-
var
|
|
664
|
-
var
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
var
|
|
968
|
+
var segs = structuredFields.splitUnquoted(p, ";");
|
|
969
|
+
var head = segs[0].trim();
|
|
970
|
+
var value = caseSensitive ? head : head.toLowerCase();
|
|
971
|
+
var q = 1;
|
|
972
|
+
// The q parameter is the named token `q` separating media-type params from
|
|
973
|
+
// accept-ext; the FIRST `q` parameter is the quality. Only a parameter
|
|
974
|
+
// literally named `q` counts — never a `q=`-shaped substring of another
|
|
975
|
+
// parameter's name or quoted value.
|
|
976
|
+
for (var s = 1; s < segs.length; s++) {
|
|
977
|
+
var kv = structuredFields.parseKeyValuePiece(segs[s], "=", true);
|
|
978
|
+
if (kv.key !== "q") continue;
|
|
979
|
+
var qm = String(kv.value).trim().match(/^([0-9]*\.?[0-9]+)/);
|
|
673
980
|
q = qm ? parseFloat(qm[1]) : 1;
|
|
674
981
|
if (isNaN(q) || q < 0) q = 0;
|
|
675
982
|
if (q > 1) q = 1;
|
|
983
|
+
break;
|
|
676
984
|
}
|
|
677
985
|
out.push({ value: value, q: q });
|
|
678
986
|
}
|
|
@@ -890,7 +1198,10 @@ module.exports = {
|
|
|
890
1198
|
parseListHeader: parseListHeader,
|
|
891
1199
|
// proxy-trust primitives (default refuses forwarded headers)
|
|
892
1200
|
clientIp: clientIp,
|
|
1201
|
+
trustedClientIp: trustedClientIp,
|
|
1202
|
+
ipPrefix: ipPrefix,
|
|
893
1203
|
requestProtocol: requestProtocol,
|
|
1204
|
+
trustedProtocol: trustedProtocol,
|
|
894
1205
|
appendVary: appendVary,
|
|
895
1206
|
// CVE-2026-21710 wrap — safe alternative to req.headersDistinct
|
|
896
1207
|
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(" / "));
|