GHSA-5mj8-gf6m-fhw8: High severity npm/9router vulnerability
Summary
9router 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.
Affected Component
- src/dashboardGuard.js - isLocalRequest() — trusts the client-supplied X-9r-Real-Ip header to decide loopback origin - canAccessPublicLlmApi() — grants access to /api/v1/ when isLocalRequest() returns true, skipping API-key validation - Verified affected route: GET /api/v1/models - Product version tested: 9router-app 0.5.4 (Next.js 16.2.9) Root Cause
The 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:
text Untrusted Client Input ↓ X-9r-Real-Ip: 127.0.0.1 ↓ isLocalRequest() → true ↓ canAccessPublicLlmApi() → allowed (API key not required) ↓ 200 OK
Attack Scenario
1. 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). 2. The attacker sends a normal request to /api/v1/models and receives 401 Unauthorized (API key required for remote access). 3. The attacker re-sends the identical request with the single added header X-9r-Real-Ip: 127.0.0.1. 4. 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.
Proof of Concept
Baseline Request <img width="1211" height="402" alt="Screenshot 2026-06-19 174727" src="https://github.com/user-attachments/assets/170b635f-1bd6-4dfd-ad85-30a03a9f6f72" />
Exploit Request
<img width="1207" height="816" alt="Screenshot 2026-06-19 174844" src="https://github.com/user-attachments/assets/b7b4efde-c071-4aa2-ab13-9bde7c88a7b8" />
The only difference between the two requests is the addition of X-9r-Real-Ip: 127.0.0.1. Impact
An 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:
- Unauthorized use of the owner's configured LLM provider connections - Consumption of paid API credits / financial loss to the instance owner - Abuse of upstream provider accounts via the proxy - Enumeration of configured providers and available models
Remediation
- Do not trust X-9r-Real-Ip (or any X-9r- header) when received directly from clients. - Derive the client address for authorization from a trusted transport-level source, e.g. req.socket.remoteAddress, rather than a request header. - 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. - Document supported, secure startup modes so the application is not run in a configuration where the header is attacker-controllable.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/9routerto a version that resolves this vulnerability.Fixed in 0.5.8 - Configuration
Derive the client address for local-request authorization from the trusted transport-level socket address (req.socket.remoteAddress), not from X-9r-Real-Ip or another client-supplied request header.
isLocalRequest() authorization logic client address source = req.socket.remoteAddress - Configuration
Explicitly strip or reject all inbound client-supplied X-9r-* headers at the edge before requests reach the application.
network edge / request handling X-9r-* inbound headers = strip or reject - Configuration
When custom-server.js is required for the security model, fail closed if its trusted marker is absent so the application cannot run with an attacker-controllable X-9r-Real-Ip header.
custom-server.js startup trusted marker validation = fail closed when absent
Event History
Frequently Asked Questions
Which deployment configurations are exposed?
Deployments in which requests reach Next.js directly are exposed, because X-9r-Real-Ip is not stripped and regenerated from the TCP socket address. The header is intended to be handled by the bundled custom-server.js layer.
What does an attacker need to exploit this issue?
A remote attacker does not need authentication or user interaction. They can send an X-9r-Real-Ip header with a loopback value such as 127.0.0.1.
What access can successful exploitation provide?
The attacker can bypass API-key validation for the public LLM API under /api/v1/*. This provides unauthenticated access to the instance owner's configured provider resources; GET /api/v1/models was verified as affected.
Which version was confirmed vulnerable?
The issue was tested and confirmed in 9router-app 0.5.4 running with Next.js 16.2.9.