Spaces:
Runtime error
Runtime error
File size: 4,229 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | import { timingSafeEqual } from "node:crypto";
import { classifyHostLocality } from "./routeGuard";
/**
* Resolve the real peer IP from the trusted `<token>|<ip>` stamp that the custom
* Node server writes into PEER_IP_HEADER (see scripts/dev/peer-stamp.mjs). Returns
* the IP ONLY when the token constant-time-matches this process's stamp token;
* any other value (no stamp, wrong/forged token, missing separator, empty IP)
* returns null.
*
* Pure + dependency-free so the auth boundary is directly unit-testable.
*
* SECURITY: this is the ONLY trustworthy locality signal in the Next middleware
* runtime (which has no socket). Never derive locality from the Host header β it
* is fully client-controlled, so `Host: 127.0.0.1` from a remote attacker would
* otherwise bypass the LOCAL_ONLY gate guarding spawn-capable routes.
*/
export function resolveStampedPeer(
headerValue: string | null,
token: string | undefined
): string | null {
if (!headerValue || !token) return null;
const sep = headerValue.indexOf("|");
if (sep <= 0) return null;
const provided = headerValue.slice(0, sep);
const ip = headerValue.slice(sep + 1);
if (!ip) return null;
if (provided.length !== token.length) return null;
try {
if (!timingSafeEqual(Buffer.from(provided), Buffer.from(token))) return null;
} catch {
return null;
}
return ip;
}
/**
* Resolve the trusted "request arrived via a reverse proxy" marker stamped by
* the custom Node server (`scripts/dev/peer-stamp.mjs::stampPeerIp`). The stamp
* is `<token>|1` when forwarding headers (`x-forwarded-for` / `x-real-ip`) were
* present on the inbound TCP request, and `<token>|0` otherwise.
*
* Returns true ONLY when the token constant-time-matches this process's stamp
* token AND the payload is exactly "1". Any other value β no stamp, forged
* token, "0", junk β returns false (the safe default: assume no proxy hop).
*
* SECURITY: paired with `resolveStampedPeer()` to close the upstream
* decolua/9router da667836 vulnerability β when OmniRoute itself runs behind
* an external reverse proxy (nginx / Caddy / Cloudflare Tunnel),
* `req.socket.remoteAddress` is the proxy hop (usually 127.0.0.1), not the
* end-user. Without this signal, `classifyHostLocality()` would return
* "loopback" for every remote caller arriving via the proxy, granting access
* to the LOCAL_ONLY tier that gates spawn-capable routes (Hard Rules #15 +
* #17). Consume via `classifyStampedPeerLocality()` below.
*/
export function resolveStampedViaProxy(
headerValue: string | null,
token: string | undefined
): boolean {
if (!headerValue || !token) return false;
const sep = headerValue.indexOf("|");
if (sep <= 0) return false;
const provided = headerValue.slice(0, sep);
const payload = headerValue.slice(sep + 1);
if (provided.length !== token.length) return false;
try {
if (!timingSafeEqual(Buffer.from(provided), Buffer.from(token))) return false;
} catch {
return false;
}
return payload === "1";
}
/**
* The trusted locality verdict consumed by the LOCAL_ONLY route guard. Wraps
* `resolveStampedPeer()` + `resolveStampedViaProxy()` + `classifyHostLocality()`
* so the pipeline has a single boundary helper:
*
* 1. Resolve the real peer IP from PEER_IP_HEADER (or fail closed β remote).
* 2. If the via-proxy marker is present, the loopback / private-LAN socket
* is the proxy hop, not the end-user β downgrade to "remote".
* (Public-IP sockets are already remote, so the marker is a no-op there.)
* 3. Otherwise classify the raw IP normally (loopback / lan / remote).
*
* Pure; both header values are token-validated, so an attacker who knows the
* header names but not the per-process token cannot influence the verdict in
* any direction.
*/
export function classifyStampedPeerLocality(
peerHeader: string | null,
viaProxyHeader: string | null,
token: string | undefined
): "loopback" | "lan" | "remote" {
const ip = resolveStampedPeer(peerHeader, token);
const viaProxy = resolveStampedViaProxy(viaProxyHeader, token);
const locality = classifyHostLocality(ip);
if (viaProxy && locality !== "remote") return "remote";
return locality;
}
|