When OmniRoute itself runs behind an external reverse proxy (nginx, Caddy,
Cloudflare Tunnel), `req.socket.remoteAddress` on the inbound connection is
the proxy hop -- usually 127.0.0.1 -- not the real end-user. The custom Node
server stamped that loopback IP as the trusted PEER_IP_HEADER, so
`classifyHostLocality()` returned "loopback" for every remote caller arriving
via the proxy. The LOCAL_ONLY route guard (which gates spawn-capable routes:
/api/mcp/, /api/cli-tools/runtime/, /api/services/, /api/plugins/,
/api/system/version, /api/tools/agent-bridge/, ...) was therefore
effectively bypassed: a leaked JWT over the public tunnel could trigger
arbitrary child-process spawning (Hard Rules #15 + #17).
Fix mirrors decolua/9router commit da667836:
1. scripts/dev/peer-stamp.mjs stamps a companion VIA_PROXY_HEADER as
`<token>|1` whenever the inbound request carries forwarding headers
(`x-forwarded-for` / `x-real-ip`), token-protected with the same
per-process secret as PEER_IP_HEADER so a remote caller cannot forge
either its presence or its absence.
2. src/server/authz/peerStamp.ts exposes a new
`classifyStampedPeerLocality(peer, viaProxy, token)` that combines the
two stamps: when the via-proxy marker is present, a loopback /
private-LAN socket is downgraded to "remote" so the LOCAL_ONLY tier is
not bypassed.
3. src/server/authz/pipeline.ts switches the AUTHZ_HEADER_PEER_LOCALITY
stamp to the new helper and strips VIA_PROXY_HEADER from the forwarded
headers alongside PEER_IP_HEADER.
4. src/server/authz/policies/management.ts gates `isLoopbackRequest()` and
`isPrivateLanRequest()` on the same via-proxy check so the management
policy carve-outs (CLI-token gate, LAN-allowed LOCAL_ONLY surface) also
refuse to honour a proxy-hop socket.
TDD: tests/unit/route-guard-loopback-via-proxy.test.ts proves a request with
`socket.remoteAddress=127.0.0.1` + a via-proxy stamp is classified "remote".
RED first, then GREEN. The local-CLI happy path (loopback socket, no proxy
stamp) is unchanged. Untokened via-proxy hints from a remote attacker are
ignored. Existing tests (route-guard-private-lan, authz/routeGuard,
authz/pipeline, authz/management-policy) all stay green.
Inspired-by: decolua/9router@da667836
Co-authored-by: decolua <decoluadt@example.com>
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!