P7 r8 fix: forward_auth trust boundary (XFF/XFH honored only from trusted proxy; direct/spoofed requests use socket identity) + adversarial tests

This commit is contained in:
Hermes committed 2026-10-03 23:25:23 -07:00
1 parent 1e606bf0f3
commit d4973f3af3
3 files changed
+90 -8

No files matched your search

+41 -6
View File
@@ -16,6 +16,10 @@
* to req.ip when the header is absent.
*
* Host: X-Forwarded-Host first (proxy-rewritten), then Host.
*
* TRUST BOUNDARY: forwarded headers are consulted only when the immediate
* peer is in trustedProxies; every other request is identified by its
* socket address alone.
*/
'use strict';
@@ -53,6 +57,7 @@ function parseForwardedFor(header) {
module.exports = function networkForwardAuthRoutesFactory({
service,
asyncHandler,
trustedProxies = null, // e.g. ['127.0.0.1', '::1'] — the Caddy proxy addrs
log = { info() {}, warn() {}, error() {} },
} = {}) {
const router = require('express').Router(); // eslint-disable-line global-require
@@ -67,16 +72,46 @@ module.exports = function networkForwardAuthRoutesFactory({
asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // eslint-disable-line no-param-reassign
}
// Trust boundary (r7 judge blocker): forwarded headers are honored ONLY
// when the immediate peer (socket remote address) is an explicitly
// configured trusted proxy (the Caddy reverse proxy on the host). A direct
// request — or one arriving via any other hop — gets its headers IGNORED,
// so a spoofed X-Forwarded-For cannot smuggle a tailnet identity.
function isTrustedPeer(req) {
if (trustedProxies === null) {
// disabled trust: never honor forwarded headers
return false;
}
const peer =
(req.socket && req.socket.remoteAddress) ||
req.connection?.remoteAddress || null;
if (!peer) return false;
const norm = peer.startsWith('::ffff:') ? peer.slice(7) : peer;
return trustedProxies.includes(norm);
}
function sourceIpFor(req) {
const chain = parseForwardedFor(req.headers['x-forwarded-for']);
if (chain.length > 0) return chain[0]; // first entry = original client
return req.ip || null;
if (isTrustedPeer(req)) {
const chain = parseForwardedFor(req.headers['x-forwarded-for']);
if (chain.length > 0) return chain[0]; // first entry = original client
}
// Untrusted/absent proxy info: the socket address is the only
// non-spoofable source. (whois of a proxy LAN IP simply resolves to
// unknown-device — safe default, never an inherited identity.)
const sock = (req.socket && req.socket.remoteAddress) ||
req.connection?.remoteAddress || req.ip || null;
if (typeof sock === 'string' && sock.startsWith('::ffff:')) {
return sock.slice(7);
}
return sock;
}
function hostFor(req) {
const fwd = req.headers['x-forwarded-host'];
if (typeof fwd === 'string' && fwd.trim().length > 0) {
return fwd.split(',')[0].trim();
if (isTrustedPeer(req)) {
const fwd = req.headers['x-forwarded-host'];
if (typeof fwd === 'string' && fwd.trim().length > 0) {
return fwd.split(',')[0].trim();
}
}
return req.headers.host || req.hostname || null;
}