Friday, 16 October 2026
1 agent hack today▼ 7 vs yesterday (8)

One spoofed header unlocks 9Router's LLM API to strangers

A GitHub advisory says 9Router treats any request carrying an X-9r-Real-Ip header of 127.0.0.1 as local, skipping the API-key check entirely.

By The Agentic Times ·

A single forged badge labeled "127.0.0.1" grants strangers passage through a locked door that should require a unique key, leaving the vault unguarded.
A single forged badge labeled "127.0.0.1" grants strangers passage through a locked door that should require a unique key, leaving the vault unguarded. · Illustration: The Agentic Times

A flaw in 9Router, an npm-distributed LLM routing app, lets an unauthenticated remote attacker bypass API-key enforcement on its public LLM API by adding a single HTTP header. The issue is tracked as CVE-2026-56681 and was published as GitHub advisory GHSA-5mj8-gf6m-fhw8 on 22 September 2026. The proof-of-concept screenshots in the advisory are dated 19 June 2026, and the tested version is 9router-app 0.5.4 running on Next.js 16.2.9.

According to the advisory, 9Router decides whether a request came from the local machine by reading the X-9r-Real-Ip request header. That header is meant to be produced and sanitised only by the bundled custom-server.js layer, which derives the value from the TCP socket address and strips any copy sent by a client. The problem appears in deployment modes where requests reach Next.js directly and that wrapper is not in the path. In those setups, Next.js passes the attacker-supplied header through unchanged.

The affected code is src/dashboardGuard.js. The advisory names two functions: isLocalRequest(), which classifies a request as local if the header holds a loopback address, and canAccessPublicLlmApi(), which grants access to /api/v1/* routes whenever isLocalRequest() returns true and skips API-key validation. The verified affected route is GET /api/v1/models.

The attack described is trivial to reproduce. The advisory says the default bind is 0.0.0.0, so the service is often reachable beyond the host. An attacker first sends a normal request to /api/v1/models and receives 401 Unauthorized, because remote requests require an API key. The attacker then resends the identical request with one extra line, X-9r-Real-Ip: 127.0.0.1, and receives 200 OK along with the owner's model catalogue. The advisory states the only difference between the baseline and exploit requests is that single header.

On impact, the advisory says an unauthenticated remote attacker who can reach the service can act as a trusted local client. The listed consequences are unauthorised use of the owner's configured LLM provider connections, consumption of paid API credits and financial loss to the instance owner, abuse of upstream provider accounts through the proxy, and enumeration of configured providers and available models. The incident record scores the severity at 4 and describes the root cause as excessive permissions. The status is recorded as reported.

The advisory's remediation advice is direct. It says not to trust X-9r-Real-Ip, or any X-9r-* header, when it arrives directly from a client. Instead, the client address used for authorisation should come from a trusted transport-level source such as req.socket.remoteAddress. If custom-server.js is required for the security model to hold, the advisory says the application should fail closed when the wrapper's trusted marker is absent, and should strip or reject inbound client-supplied X-9r-* headers at the edge. It also recommends documenting which startup modes are supported and secure, so operators do not accidentally run the application in a configuration where the header is attacker-controllable.

The pattern is familiar to anyone who has audited reverse-proxy deployments: a header that is safe inside a trusted chain becomes a free pass once the chain is broken. Here the assumption was never enforced in code, only in documentation of how the app should be launched.

Sources

  1. github.comhttps://github.com/advisories/GHSA-5mj8-gf6m-fhw8