GHSA-X5C9-V98J-722R
Vulnerability from github – Published: 2026-09-23 18:12 – Updated: 2026-09-23 18:12Summary
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. - 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:
bash docker compose up --build - Direct baseline (no API key):
bash curl -i http://127.0.0.1:18081/v1/models - Direct spoof control:
bash 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):
bash curl -i http://127.0.0.1:18080/v1/models - Reverse-proxy
Origincontrol:bash 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.4.80"
},
"package": {
"ecosystem": "npm",
"name": "9router"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.5.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56675"
],
"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"
},
"details": "## Summary\n\n9router treats local loopback requests as trusted and allows access to `/v1/*` without an\nAPI key. In a documented/common reverse-proxy deployment where nginx forwards public\ntraffic to the backend via `127.0.0.1`, external non-`Origin` requests are misclassified as\nlocal. This allows unauthenticated access to `/v1` APIs such as `/v1/models`, and may allow\nabuse of configured upstream provider credentials depending on the enabled providers.\n\n## Details\n\n- **Affected version / commit:** 9router `v0.4.80` @ `b282f05`.\n- **Deployment precondition:** a same-host reverse proxy (e.g. nginx) forwarding public\n traffic to the backend on `127.0.0.1` / `localhost`. This mirrors the documented cloud\n deployment (`proxy_pass http://localhost:20128` with `X-Real-IP` / `X-Forwarded-For`).\n- **Observed behaviour:**\n - The **direct backend** (`direct-backend`, port `18081`) returns `401` for `/v1/models`\n without an API key.\n - A **direct request that spoofs** `X-9r-Real-IP: 127.0.0.1` still returns `401`: the\n custom server deletes the client-supplied header and overwrites it with the real socket\n address, so naive header spoofing does not work against the direct backend.\n - The **proxied path** (`reverse-proxy`, port `18080`) returns `200` with the full model\n catalog for the same `/v1/models` request **without any API key**.\n - A **proxied request that carries an `Origin` header** returns `401`. The bypass\n therefore primarily affects curl / SDK / server-side / non-browser clients, which do\n not send `Origin`.\n- **Root cause:** the backend\u0027s local/remote decision relies on perceived socket/loopback\n locality after reverse proxying. Because nginx connects to the backend from `127.0.0.1`,\n the backend stamps a loopback client address for **every** internet client and treats the\n request as local, skipping the `/v1` API-key requirement. The forwarded `X-Real-IP` /\n `X-Forwarded-For` headers that carry the true client IP are ignored for this decision.\n- This is **not** a simple client header-spoofing issue (the direct-spoof control above\n proves header spoofing is rejected); it is a property of how loopback proxy traffic is\n trusted.\n\n## Proof of Concept\n\nThis repository is a self-contained Docker Compose reproduction. No real provider is called\nand no real API key is required.\n\n1. Build and start the stack:\n ```bash\n docker compose up --build\n ```\n2. Direct baseline (no API key):\n ```bash\n curl -i http://127.0.0.1:18081/v1/models\n ```\n3. Direct spoof control:\n ```bash\n curl -i -H \"X-9r-Real-IP: 127.0.0.1\" http://127.0.0.1:18081/v1/models\n ```\n4. Reverse-proxy bypass (no API key):\n ```bash\n curl -i http://127.0.0.1:18080/v1/models\n ```\n5. Reverse-proxy `Origin` control:\n ```bash\n curl -i -H \"Origin: http://evil.example\" http://127.0.0.1:18080/v1/models\n ```\n\n### Expected evidence\n\n| Request | Result |\n|---------|--------|\n| Direct `18081`, no key | `401 Unauthorized` (`{\"error\":\"API key required for remote API access\"}`) |\n| Direct `18081`, `X-9r-Real-IP: 127.0.0.1` spoof | `401 Unauthorized` |\n| Proxied `18080`, no key | `200 OK` with the full model catalog |\n| Proxied `18080`, with `Origin` | `401 Unauthorized` |\n\n## Impact\n\n- Unauthenticated access to the `/v1` API surface in the affected reverse-proxy deployment.\n- Model enumeration via `/v1/models`.\n- Possible abuse of the operator\u0027s configured upstream provider credentials through\n `/v1/chat/completions` and other `/v1` proxy endpoints (the attacker spends the operator\u0027s\n provider quota/keys without holding any key of their own).\n- Actual impact depends on which providers are configured and how the instance is exposed\n to the public internet.\n- The attacker requires **no API key**.\n\n## Suggested Fix\n\n- Do not use client/proxy/socket IP locality as an authentication bypass.\n- Require an API key by default for `/v1/*` on public listeners.\n- If local trust is genuinely needed, bind it to an unguessable server-generated secret or\n to a Unix domain socket that is only accessible locally \u2014 not to \"the connection looks\n like loopback\".\n- When running behind reverse proxies, use an explicit trusted-proxy configuration and a\n real client-IP derivation (e.g. a vetted `X-Forwarded-For` chain), and never treat all\n loopback proxy traffic as end-user-local.\n- Document a secure reverse-proxy configuration for operators.",
"id": "GHSA-x5c9-v98j-722r",
"modified": "2026-09-23T18:12:26Z",
"published": "2026-09-23T18:12:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/decolua/9router/security/advisories/GHSA-x5c9-v98j-722r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56675"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/commit/da667836cc7584bea0edd893de1d590c9ea279dc"
},
{
"type": "PACKAGE",
"url": "https://github.com/decolua/9router"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/releases/tag/v0.5.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "9router /v1 APIs has unauthenticated access via reverse proxy locality collapse"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.