Summary
9router /v1 APIs has unauthenticated access via reverse proxy locality collapse
Advisory details
Summary
9router treats local loopback requests as trusted and allows access to /v1/* without an
API key. In a documented/common reverse-proxy deployment where nginx forwards public
traffic to the backend via 127.0.0.1, external non-Origin requests are misclassified as
local. This allows unauthenticated access to /v1 APIs such as /v1/models, and may allow
abuse of configured upstream provider credentials depending on the enabled providers.
Details
- Affected version / commit: 9router
v0.4.80@b282f05. - Deployment precondition: a same-host reverse proxy (e.g. nginx) forwarding public
traffic to the backend on
127.0.0.1/localhost. This mirrors the documented cloud deployment (proxy_pass http://localhost:20128withX-Real-IP/X-Forwarded-For). - Observed behaviour:
- The direct backend (
direct-backend, port18081) returns401for/v1/modelswithout an API key. - A direct request that spoofs
X-9r-Real-IP: 127.0.0.1still returns401: the custom server deletes the client-supplied header and overwrites it with the real socket address, so naive header spoofing does not work against the direct backend. - The proxied path (
reverse-proxy, port18080) returns200with the full model catalog for the same/v1/modelsrequest without any API key. - A proxied request that carries an
Originheader returns401. The bypass therefore primarily affects curl / SDK / server-side / non-browser clients, which do not sendOrigin.
- The direct backend (
- Root cause: the backend's local/remote decision relies on perceived socket/loopback
locality after reverse proxying. Because nginx connects to the backend from
127.0.0.1, the backend stamps a loopback client address for every internet client and treats the request as local, skipping the/v1API-key requirement. The forwardedX-Real-IP/X-Forwarded-Forheaders that carry the true client IP are ignored for this decision. - This is not a simple client header-spoofing issue (the direct-spoof control above proves header spoofing is rejected); it is a property of how loopback proxy traffic is trusted.
Proof of Concept
This repository is a self-contained Docker Compose reproduction. No real provider is called and no real API key is required.
- Build and start the stack:
docker compose up --build - Direct baseline (no API key):
curl -i http://127.0.0.1:18081/v1/models - Direct spoof control:
curl -i -H "X-9r-Real-IP: 127.0.0.1" http://127.0.0.1:18081/v1/models - Reverse-proxy bypass (no API key):
curl -i http://127.0.0.1:18080/v1/models - Reverse-proxy
Origincontrol:curl -i -H "Origin: http://evil.example" http://127.0.0.1:18080/v1/models
Expected evidence
| Request | Result |
|---|---|
Direct 18081, no key |
401 Unauthorized ({"error":"API key required for remote API access"}) |
Direct 18081, X-9r-Real-IP: 127.0.0.1 spoof |
401 Unauthorized |
Proxied 18080, no key |
200 OK with the full model catalog |
Proxied 18080, with Origin |
401 Unauthorized |
Impact
- Unauthenticated access to the
/v1API surface in the affected reverse-proxy deployment. - Model enumeration via
/v1/models. - Possible abuse of the operator's configured upstream provider credentials through
/v1/chat/completionsand other/v1proxy endpoints (the attacker spends the operator's provider quota/keys without holding any key of their own). - Actual impact depends on which providers are configured and how the instance is exposed to the public internet.
- The attacker requires no API key.
Suggested Fix
- Do not use client/proxy/socket IP locality as an authentication bypass.
- Require an API key by default for
/v1/*on public listeners. - If local trust is genuinely needed, bind it to an unguessable server-generated secret or to a Unix domain socket that is only accessible locally — not to "the connection looks like loopback".
- When running behind reverse proxies, use an explicit trusted-proxy configuration and a
real client-IP derivation (e.g. a vetted
X-Forwarded-Forchain), and never treat all loopback proxy traffic as end-user-local. - Document a secure reverse-proxy configuration for operators.
References
- https://github.com/advisories/GHSA-x5c9-v98j-722r
- https://github.com/decolua/9router/security/advisories/GHSA-x5c9-v98j-722r
- https://nvd.nist.gov/vuln/detail/CVE-2026-56675
- https://github.com/decolua/9router/commit/da667836cc7584bea0edd893de1d590c9ea279dc
- https://github.com/decolua/9router/releases/tag/v0.5.2
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-84313
Nginx UI: TOTP / passkey enrolment requires no re-authentication, enabling persistent account takeover
- HIGHCVE-2026-62367
Vikunja: OIDC email-fallback account linking ignores email_verified, enabling local-account takeover
- HIGHCVE-2026-61435
PraisonAI: Call API localhost-only authentication bypass via spoofed Host header
- HIGHCVE-2026-61436
PraisonAI: AgentMail webhook mode accepts forged unsigned message.received events and invokes agents
- CRITICALCVE-2026-88018
rclone serve s3: --auth-proxy without --auth-key authenticates nobody - full SigV4 signature bypass
- MEDIUMCVE-2026-73840
OpenChoreo: Unauthenticated build/workflow trigger via git-provider confusion (webhook signature bypass)