@blamejs/core
Advanced tools
+54
-31
@@ -147,29 +147,15 @@ "use strict"; | ||
| function _reconstructHtu(req, mopts) { | ||
| // The proof's htu is the request URI WITHOUT query/fragment. Behind | ||
| // a reverse proxy the operator may need to override via opts.htu / | ||
| // opts.getHtu. X-Forwarded-* headers are ATTACKER-CONTROLLED when | ||
| // the origin is reachable directly; an attacker who can hit the | ||
| // origin while spoofing X-Forwarded-Proto: https can trick this | ||
| // function into building an `https` htu that the DPoP proof was | ||
| // signed for — when the origin is actually serving HTTP. RFC 9449 | ||
| // §4.3 says htu MUST be the absolute URL the request was sent to. | ||
| // | ||
| // Default: ignore X-Forwarded-* and derive proto/host from the | ||
| // socket. Operators with a confirmed-trusted front proxy opt in | ||
| // via opts.trustForwardedHeaders: true. | ||
| mopts = mopts || {}; | ||
| var trustForwarded = mopts.trustForwardedHeaders === true; | ||
| var proto; | ||
| if (trustForwarded && req.headers["x-forwarded-proto"]) { | ||
| proto = String(req.headers["x-forwarded-proto"]).split(",")[0].trim(); | ||
| } else { | ||
| proto = req.socket && req.socket.encrypted ? "https" : "http"; | ||
| } | ||
| var host; | ||
| if (trustForwarded && req.headers["x-forwarded-host"]) { | ||
| host = String(req.headers["x-forwarded-host"]).split(",")[0].trim(); | ||
| } else { | ||
| host = req.headers.host; | ||
| } | ||
| function _reconstructHtu(req, protoResolver, hostResolver) { | ||
| // The proof's htu is the request URI WITHOUT query/fragment. Behind a | ||
| // reverse proxy the operator may override via opts.getHtu. RFC 9449 §4.3 | ||
| // says htu MUST be the absolute URL the request was sent to — and it is | ||
| // cryptographically bound in the proof, so a forged scheme/authority lets a | ||
| // proof signed for one origin validate against another. proto + host are | ||
| // resolved through the peer-gated requestHelpers resolvers built in create(): | ||
| // X-Forwarded-Proto / -Host are honored only from a declared trusted-proxy | ||
| // peer; otherwise the real TLS socket scheme + the request's own Host are | ||
| // used and forged forwarded headers are ignored. | ||
| if (!req || !req.headers) return null; | ||
| var proto = protoResolver.resolve(req); | ||
| var host = hostResolver.resolve(req); | ||
| if (!host) return null; | ||
@@ -209,2 +195,5 @@ var path = req.url || "/"; | ||
| * getHtu: function(req): string, | ||
| * trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-Proto + X-Forwarded-Host for htu reconstruction | ||
| * protocolResolver: function(req): "http"|"https", // own the scheme decision | ||
| * hostResolver: function(req): string|null, // own the authority decision | ||
| * nonceStore: object, | ||
@@ -233,5 +222,7 @@ * nonceWindowSec: number, | ||
| "nonceStore", "nonceWindowSec", "nonceRotateSec", "requireNonce", | ||
| // v0.9.4 — opt-in trust gate for X-Forwarded-Proto/Host when | ||
| // reconstructing htu. Default off; operators | ||
| // with a confirmed-trusted front proxy set this to `true`. | ||
| // htu reconstruction trust. trustedProxies (CIDRs) peer-gates | ||
| // X-Forwarded-Proto + X-Forwarded-Host; protocolResolver/hostResolver let | ||
| // the operator own each. trustForwardedHeaders (legacy boolean) is refused | ||
| // on its own — see the peer-gating block below. | ||
| "trustedProxies", "protocolResolver", "hostResolver", | ||
| "trustForwardedHeaders", "onDeny", "problemDetails", | ||
@@ -288,2 +279,34 @@ ], "middleware.dpop"); | ||
| // htu reconstruction (RFC 9449 §4.3) builds the absolute request URL — | ||
| // proto + host — that the proof's cryptographically-bound `htu` claim is | ||
| // verified against. Behind a proxy both come from forgeable X-Forwarded-* | ||
| // headers, so resolve them through the peer-gated requestHelpers primitives | ||
| // (the same fail-closed model csrf-protect / security-headers / cors use): | ||
| // X-Forwarded-Proto / -Host are honored ONLY when the immediate peer is a | ||
| // declared trusted proxy. The legacy trustForwardedHeaders:true trusted the | ||
| // headers from ANY caller — a direct attacker could forge XFP:https / a | ||
| // victim XFH to make a proof signed for one origin validate against another | ||
| // (htu confusion). It is refused on its own; migrate to trustedProxies. | ||
| var _proto = requestHelpers.trustedProtocol({ | ||
| trustedProxies: opts.trustedProxies, | ||
| protocolResolver: opts.protocolResolver, | ||
| }); | ||
| var _host = requestHelpers.trustedHost({ | ||
| trustedProxies: opts.trustedProxies, | ||
| hostResolver: opts.hostResolver, | ||
| }); | ||
| // Only refuse the spoofable legacy flag when the htu is actually | ||
| // reconstructed from the request. When the operator supplies getHtu they own | ||
| // the entire URI, _reconstructHtu (and the forwarded headers) is never | ||
| // consulted, so a leftover trustForwardedHeaders is moot — don't fail | ||
| // construction on it (the error text even offers getHtu as a migration path). | ||
| if (typeof opts.getHtu !== "function" && opts.trustForwardedHeaders === true && !_proto.peerGated) { | ||
| throw new AuthError("auth-dpop/bad-opt", | ||
| "middleware.dpop: trustForwardedHeaders is spoofable for the htu reconstruction " + | ||
| "(a direct caller can forge X-Forwarded-Proto / X-Forwarded-Host) and is no longer " + | ||
| "honored on its own. Declare your reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] " + | ||
| "(peer-gates X-Forwarded-Proto + X-Forwarded-Host), or own the decision via " + | ||
| "protocolResolver(req) / hostResolver(req) / getHtu(req)."); | ||
| } | ||
| function _freshNonce() { return nonceMgr ? nonceMgr.issue() : null; } | ||
@@ -316,3 +339,3 @@ | ||
| var htu = (typeof opts.getHtu === "function" ? opts.getHtu(req) : _reconstructHtu(req, opts)); | ||
| var htu = (typeof opts.getHtu === "function" ? opts.getHtu(req) : _reconstructHtu(req, _proto, _host)); | ||
| if (!htu) { | ||
@@ -319,0 +342,0 @@ return _writeUnauthorized(req, res, "invalid_dpop_proof", "could not reconstruct htu", null, onDeny, problemMode); |
@@ -71,2 +71,7 @@ "use strict"; | ||
| function _scheme(req) { | ||
| // Display-only: the OTel url.scheme span attribute reflects the scheme the | ||
| // client used (forwarded), NOT a Secure/HSTS/origin trust decision. Routing | ||
| // through trustedProtocol would drop the forwarded scheme from spans behind a | ||
| // proxy (less accurate telemetry) for no security gain. | ||
| // allow:raw-xfp — telemetry label, not a trust sink (see above). | ||
| var x = req.headers && (req.headers["x-forwarded-proto"] || ""); | ||
@@ -81,2 +86,4 @@ if (typeof x === "string" && x.length > 0) { | ||
| function _serverAddress(req) { | ||
| // allow:raw-xfp — display-only: server.address span attribute (telemetry), | ||
| // not an authority trust decision. Same rationale as _scheme above. | ||
| var hostHeader = req.headers && (req.headers["x-forwarded-host"] || req.headers.host); | ||
@@ -83,0 +90,0 @@ if (typeof hostHeader === "string" && hostHeader.length > 0) { |
@@ -639,2 +639,90 @@ "use strict"; | ||
| /** | ||
| * @primitive b.requestHelpers.trustedHost | ||
| * @signature b.requestHelpers.trustedHost(opts?) | ||
| * @since 0.15.18 | ||
| * @related b.requestHelpers.requestHost, b.requestHelpers.trustedProtocol | ||
| * | ||
| * Peer-gated companion to trustedProtocol for the request authority (host). | ||
| * Reconstructing the absolute request URL — the DPoP `htu`, an origin/issuer | ||
| * string, a redirect base — depends on the host the client addressed; behind a | ||
| * proxy that comes from X-Forwarded-Host, which is forgeable unless the | ||
| * immediate peer is a trusted proxy. Returns `{ resolve(req)=>string|null, | ||
| * peerGated }`. With `trustedProxies` (CIDRs) X-Forwarded-Host is honored only | ||
| * from a trusted peer; with `hostResolver(req)` the operator owns it; with | ||
| * neither only the request's own Host header is used (forwarded host ignored). | ||
| * | ||
| * @opts | ||
| * trustedProxies: string | string[], | ||
| * hostResolver: function(req): string|null, | ||
| * | ||
| * @example | ||
| * var th = b.requestHelpers.trustedHost({ trustedProxies: ["10.0.0.0/8"] }); | ||
| * th.resolve(req); // X-Forwarded-Host only when it came via a trusted peer | ||
| */ | ||
| function trustedHost(opts) { | ||
| opts = opts || {}; | ||
| var resolver = opts.hostResolver; | ||
| if (resolver != null && typeof resolver !== "function") { | ||
| throw new TypeError("trustedHost: hostResolver must be a function(req) => string|null"); | ||
| } | ||
| var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedHost"); | ||
| return { | ||
| peerGated: !!(resolver || predicate), | ||
| resolve: function (req) { | ||
| if (resolver) return resolver(req); | ||
| if (predicate) return requestHost(req, { trustProxy: predicate }); | ||
| return requestHost(req, { trustProxy: false }); | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * @primitive b.requestHelpers.requestHost | ||
| * @signature b.requestHelpers.requestHost(req, opts?) | ||
| * @since 0.15.18 | ||
| * @related b.requestHelpers.requestProtocol, b.requestHelpers.trustedHost | ||
| * | ||
| * Resolve the inbound authority (host[:port]). Default returns the request's | ||
| * own `Host` header. Behind a trusted reverse proxy that rewrites the host, | ||
| * pass `trustProxy` as a PREDICATE `function(addr)=>boolean` (build it via | ||
| * `b.requestHelpers.trustedHost`): `X-Forwarded-Host` is then honored only when | ||
| * the immediate peer is a trusted proxy, so a direct caller can't forge it. The | ||
| * legacy `trustProxy: true` reads the leftmost forwarded hop without checking | ||
| * the peer — forgeable. Returns the host string, or `null` when absent. | ||
| * | ||
| * @opts | ||
| * trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true | ||
| * | ||
| * @example | ||
| * b.requestHelpers.requestHost({ headers: { host: "app.example.com" } }); | ||
| * // → "app.example.com" | ||
| */ | ||
| function requestHost(req, opts) { | ||
| if (!req || !req.headers) return null; | ||
| var trust = opts && opts.trustProxy; | ||
| if (trust) { | ||
| var fwd = req.headers["x-forwarded-host"]; | ||
| if (typeof fwd === "string" && fwd.length > 0) { | ||
| var hops = parseListHeader(fwd); | ||
| if (hops.length > 0) { | ||
| if (typeof trust === "function") { | ||
| // Peer-gated: honor X-Forwarded-Host only when the immediate TCP peer | ||
| // is a trusted proxy. A direct caller's forged header is ignored — | ||
| // fall through to the request's own Host header. | ||
| var peer = | ||
| (req.socket && typeof req.socket.remoteAddress === "string" && req.socket.remoteAddress) ? req.socket.remoteAddress | ||
| : (req.connection && typeof req.connection.remoteAddress === "string" && req.connection.remoteAddress) ? req.connection.remoteAddress | ||
| : null; | ||
| if (peer && trust(peer)) return hops[0]; | ||
| // peer not a trusted proxy → ignore forgeable header, fall through | ||
| } else { | ||
| return hops[0]; // legacy true — spoofable, see docstring | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return typeof req.headers.host === "string" ? req.headers.host : null; | ||
| } | ||
| // RFC 9110 §5.6.2 token grammar — letters, digits, and the | ||
@@ -1225,2 +1313,4 @@ // punctuation set `!#$%&'*+-.^_`|~`. Used by header-list parsers | ||
| trustedProtocol: trustedProtocol, | ||
| requestHost: requestHost, | ||
| trustedHost: trustedHost, | ||
| appendVary: appendVary, | ||
@@ -1227,0 +1317,0 @@ // CVE-2026-21710 wrap — safe alternative to req.headersDistinct |
+1
-1
| { | ||
| "name": "@blamejs/core", | ||
| "version": "0.15.17", | ||
| "version": "0.15.18", | ||
| "description": "The Node framework that owns its stack.", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
+6
-6
@@ -5,6 +5,6 @@ { | ||
| "specVersion": "1.5", | ||
| "serialNumber": "urn:uuid:39056bfa-d578-4577-96e0-d3ec1956c873", | ||
| "serialNumber": "urn:uuid:c814ff7e-59d1-4c8b-bb90-fe001973e26d", | ||
| "version": 1, | ||
| "metadata": { | ||
| "timestamp": "2026-06-22T18:34:01.428Z", | ||
| "timestamp": "2026-06-23T03:53:58.941Z", | ||
| "lifecycles": [ | ||
@@ -23,10 +23,10 @@ { | ||
| "component": { | ||
| "bom-ref": "@blamejs/core@0.15.17", | ||
| "bom-ref": "@blamejs/core@0.15.18", | ||
| "type": "application", | ||
| "name": "blamejs", | ||
| "version": "0.15.17", | ||
| "version": "0.15.18", | ||
| "scope": "required", | ||
| "author": "blamejs contributors", | ||
| "description": "The Node framework that owns its stack.", | ||
| "purl": "pkg:npm/%40blamejs/core@0.15.17", | ||
| "purl": "pkg:npm/%40blamejs/core@0.15.18", | ||
| "properties": [], | ||
@@ -59,3 +59,3 @@ "externalReferences": [ | ||
| { | ||
| "ref": "@blamejs/core@0.15.17", | ||
| "ref": "@blamejs/core@0.15.18", | ||
| "dependsOn": [] | ||
@@ -62,0 +62,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
17170537
0.08%299571
0.04%