{"id":"GHSA-5mj8-gf6m-fhw8","summary":"9Router has an Authentication Bypass in Public LLM API via Spoofable X-9r-Real-Ip Header","details":"## Summary\n\n9router determines whether an incoming request originates from localhost by trusting the X-9r-Real-Ip HTTP request header. This header is intended to be produced and sanitized exclusively by the bundled custom-server.js layer from the TCP socket address. In deployment modes where requests reach Next.js directly (the header is never stripped/regenerated), a remote, unauthenticated attacker can simply send X-9r-Real-Ip: 127.0.0.1 and be treated as a local client. This bypasses the API-key requirement on the public LLM API (/api/v1/*), granting unauthenticated access to the instance owner's configured provider resources.\n\n## Affected Component\n\n- src/dashboardGuard.js  \n- isLocalRequest() — trusts the client-supplied X-9r-Real-Ip header to decide loopback origin  \n- canAccessPublicLlmApi() — grants access to /api/v1/* when isLocalRequest() returns true, skipping API-key validation  \n- Verified affected route: GET /api/v1/models  \n- Product version tested: 9router-app 0.5.4 (Next.js 16.2.9)\n## Root Cause\n\nThe authorization layer makes a security decision based on a client-controllable HTTP header. isLocalRequest() reads X-9r-Real-Ip and, if its value is a loopback address (127.0.0.1), classifies the request as local. The design assumes this header can only be set by the trusted custom-server.js wrapper (which derives it from the unspoofable socket address and strips any inbound copy). When the application is served without that wrapper, Next.js passes the attacker-supplied header through unchanged, so the trust assumption is violated:\n\n```text\nUntrusted Client Input\n        ↓\nX-9r-Real-Ip: 127.0.0.1\n        ↓\nisLocalRequest()  → true\n        ↓\ncanAccessPublicLlmApi()  → allowed (API key not required)\n        ↓\n200 OK\n```\n\n## Attack Scenario\n\n1. The instance is deployed in a mode that does not use custom-server.js, and the LLM API is reachable by the attacker (the default bind is 0.0.0.0).\n2. The attacker sends a normal request to /api/v1/models and receives 401 Unauthorized (API key required for remote access).\n3. The attacker re-sends the identical request with the single added header X-9r-Real-Ip: 127.0.0.1.\n4. The request is classified as local, the API-key check is skipped, and the attacker receives 200 OK with the owner's model catalog and ongoing access to the LLM API.\n\n## Proof of Concept\n\n### Baseline Request\n\u003cimg width=\"1211\" height=\"402\" alt=\"Screenshot 2026-06-19 174727\" src=\"https://github.com/user-attachments/assets/170b635f-1bd6-4dfd-ad85-30a03a9f6f72\" /\u003e\n\n### Exploit Request\n\n\u003cimg width=\"1207\" height=\"816\" alt=\"Screenshot 2026-06-19 174844\" src=\"https://github.com/user-attachments/assets/b7b4efde-c071-4aa2-ab13-9bde7c88a7b8\" /\u003e\n\n\nThe only difference between the two requests is the addition of X-9r-Real-Ip: 127.0.0.1.\n## Impact\n\nAn unauthenticated remote attacker who can reach the service can bypass API-key enforcement on the public LLM API and act as a trusted local client. Consequences include:\n\n- Unauthorized use of the owner's configured LLM provider connections\n- Consumption of paid API credits / financial loss to the instance owner\n- Abuse of upstream provider accounts via the proxy\n- Enumeration of configured providers and available models\n\n## Remediation\n\n- Do not trust X-9r-Real-Ip (or any X-9r-* header) when received directly from clients.\n- Derive the client address for authorization from a trusted transport-level source, e.g. req.socket.remoteAddress, rather than a request header.\n- If custom-server.js is required for the security model, fail closed when its trusted marker is absent, and explicitly strip/reject any inbound client-supplied X-9r-* headers at the edge.\n- Document supported, secure startup modes so the application is not run in a configuration where the header is attacker-controllable.","aliases":["CVE-2026-56681"],"modified":"2026-09-22T17:15:10.402561848Z","published":"2026-09-22T16:34:06Z","database_specific":{"github_reviewed_at":"2026-09-22T16:34:06Z","nvd_published_at":null,"cwe_ids":["CWE-807"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/decolua/9router/security/advisories/GHSA-5mj8-gf6m-fhw8"},{"type":"WEB","url":"https://github.com/decolua/9router/commit/efd20be8d81ef2e256a7037f3aa78e6b567b5fd3"},{"type":"PACKAGE","url":"https://github.com/decolua/9router"},{"type":"WEB","url":"https://github.com/decolua/9router/releases/tag/v0.5.6"}],"affected":[{"package":{"name":"9router","ecosystem":"npm","purl":"pkg:npm/9router"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.5.8"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 0.5.4","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-5mj8-gf6m-fhw8/GHSA-5mj8-gf6m-fhw8.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L"}]}