---
id: CVE-2026-56675
aliases:
  - GHSA-x5c9-v98j-722r
title: >-
  9router /v1 APIs has unauthenticated access via reverse proxy locality
  collapse
summary: >-
  9router /v1 APIs has unauthenticated access via reverse proxy locality
  collapse
severity: high
cvss: 8.3
cwe:
  - CWE-287
  - CWE-290
  - CWE-306
  - CWE-441
vendor: 9router
product: 9router
ecosystem: npm
affected:
  - 9router <= 0.4.80
patched:
  - 9router 0.5.2
published: '2026-09-23'
updated: '2026-09-23'
sourceUpdated: '2026-09-23T18:12:27Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-x5c9-v98j-722r'
references:
  - url: 'https://github.com/decolua/9router/security/advisories/GHSA-x5c9-v98j-722r'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-56675'
  - url: >-
      https://github.com/decolua/9router/commit/da667836cc7584bea0edd893de1d590c9ea279dc
  - url: 'https://github.com/decolua/9router/releases/tag/v0.5.2'
  - url: 'https://github.com/advisories/GHSA-x5c9-v98j-722r'
tags:
  - ghsa
  - npm
epss: 0.00501
epssPercentile: 0.40347
ingestedAt: '2026-09-23T18:29:33.152Z'
---

## Overview

## 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:
   ```bash
   docker compose up --build
   ```
2. Direct baseline (no API key):
   ```bash
   curl -i http://127.0.0.1:18081/v1/models
   ```
3. Direct spoof control:
   ```bash
   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):
   ```bash
   curl -i http://127.0.0.1:18080/v1/models
   ```
5. Reverse-proxy `Origin` control:
   ```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 `/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 packages

- `9router <= 0.4.80`

## Remediation

Upgrade to a patched release:

- `9router 0.5.2`
