GHSA-x5c9-v98j-722rHighCVSS 8.3

9router /v1 APIs has unauthenticated access via reverse proxy locality collapse

Published
September 23, 2026
Last Modified
September 23, 2026

🔗 CVE IDs covered (1)

📋 Description

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:20128 with X-Real-IP / X-Forwarded-For).
  • Observed behaviour:
    • The direct backend (direct-backend, port 18081) returns 401 for /v1/models without an API key.
    • A direct request that spoofs X-9r-Real-IP: 127.0.0.1 still returns 401: 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, port 18080) returns 200 with the full model catalog for the same /v1/models request without any API key.
    • A proxied request that carries an Origin header returns 401. The bypass therefore primarily affects curl / SDK / server-side / non-browser clients, which do not send Origin.
  • 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 /v1 API-key requirement. The forwarded X-Real-IP / X-Forwarded-For headers 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.

  1. Build and start the stack:
    docker compose up --build
    
  2. Direct baseline (no API key):
    curl -i http://127.0.0.1:18081/v1/models
    
  3. Direct spoof control:
    curl -i -H "X-9r-Real-IP: 127.0.0.1" http://127.0.0.1:18081/v1/models
    
  4. Reverse-proxy bypass (no API key):
    curl -i http://127.0.0.1:18080/v1/models
    
  5. Reverse-proxy Origin control:
    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 /v1 API 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/completions and other /v1 proxy 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-For chain), and never treat all loopback proxy traffic as end-user-local.
  • Document a secure reverse-proxy configuration for operators.

🎯 Affected products1

  • npm/9router:<= 0.4.80

🔗 References (5)