GHSA-x5c9-v98j-722r

Suggest an improvement
Source
https://github.com/advisories/GHSA-x5c9-v98j-722r
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-x5c9-v98j-722r/GHSA-x5c9-v98j-722r.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-x5c9-v98j-722r
Aliases
Published
2026-09-23T18:12:26Z
Modified
2026-09-23T18:27:57Z
Severity
  • 8.3 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L CVSS Calculator
Summary
9router /v1 APIs has unauthenticated access via reverse proxy locality collapse
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: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.
Database specific
{
    "cwe_ids":  [
        "CWE-287",
        "CWE-290",
        "CWE-306",
        "CWE-441"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-09-23T18:12:26Z",
    "nvd_published_at":  "2026-07-10T17:17:01Z",
    "severity":  "HIGH"
}
References

Affected packages

npm / 9router

Package

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
0.5.2

Database specific

last_known_affected_version_range
"<= 0.4.80"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-x5c9-v98j-722r/GHSA-x5c9-v98j-722r.json"