Описание
9router: Login brute-force protection bypass via spoofed X-Forwarded-For header
Summary
The 9router dashboard login rate limiter derives the client identity from the attacker-controlled X-Forwarded-For HTTP header. When 9router is directly exposed, or deployed behind a reverse proxy that does not overwrite untrusted forwarding headers, a remote attacker can rotate the X-Forwarded-For value on each login attempt and receive a fresh rate-limit bucket every time.
This bypasses the dashboard brute-force protection and makes the login lockout mechanism ineffective.
Details
| Component | File | Note |
|---|---|---|
| Dashboard login rate limiter | src/lib/auth/loginLimiter.js | Uses X-Forwarded-For as the client identity without a trusted-proxy check |
| Dashboard login route | src/app/api/auth/login/route.js | Calls checkLock() and recordFail() using the spoofable client identity |
Vulnerable Code
src/lib/auth/loginLimiter.js:
The returned value is used as the key for the in-memory rate-limit state:
The login route uses this value when checking and recording failed login attempts:
Because X-Forwarded-For is accepted directly from the request, each unique header value creates a new rate-limit bucket with zero previous failures. An attacker can therefore bypass both the 5-attempt threshold and the progressive lockout durations.
PoC
Step 1 — Baseline: rate limiter triggers when the client identity is stable
Send repeated failed login attempts with the same X-Forwarded-For value:
Observed behavior:
| Attempt | Response |
|---|---|
| 1 | Invalid password. 4 attempt(s) left before lockout. |
| 2 | Invalid password. 3 attempt(s) left before lockout. |
| 3 | Invalid password. 2 attempt(s) left before lockout. |
| 4 | Invalid password. 1 attempt(s) left before lockout. |
| 5 | Too many failed attempts. Try again in 30s. |
| 6 | Too many failed attempts. Try again in 30s. |
This confirms that the lockout logic works when all attempts are assigned to the same rate-limit bucket.
Step 2 — Bypass: rotate X-Forwarded-For on each request
Send failed login attempts while changing the X-Forwarded-For value for every request:
Observed response for every request:
The counter resets to the initial state on every request, and the lockout is never triggered.
Step 3 — Impact amplifier: default dashboard password
If the instance is still using the default dashboard password, the rate-limit bypass allows an attacker to avoid lockout while attempting to authenticate.
Example request:
Observed response on a default installation:
The default password is an impact amplifier, not the root cause. Even if an administrator changes the password, the rate limiter remains structurally bypassable because the attacker controls the rate-limit key.
Attack Scenario
- A remote attacker identifies a publicly reachable 9router dashboard.
- The attacker sends repeated login attempts to
/api/auth/login. - For each attempt, the attacker changes the
X-Forwarded-Forheader value. - 9router treats each request as a different client and assigns a fresh rate-limit bucket.
- The attacker can continue brute-force attempts without triggering the configured lockout.
- If the instance uses a weak or default dashboard password, the attacker can gain administrative access.
Impact
A successful attacker can bypass the dashboard login lockout mechanism and perform unlimited brute-force attempts against the 9router dashboard password.
If authentication succeeds, the attacker can gain administrative access to the 9router dashboard and may be able to:
- Access configured provider credentials and API keys.
- Change dashboard and authentication settings.
- Disable login protection if the application allows it.
- Create persistent API keys or other long-lived access tokens.
- Modify application configuration.
- Chain the access with other server-side functionality exposed by the dashboard.
Пакеты
9router
<= 0.4.71
0.4.77
Связанные уязвимости
9Router is an AI router & token saver. Prior to 0.4.80, the dashboard login rate limiter in src/lib/auth/loginLimiter.js derives the client identity from the attacker-controlled X-Forwarded-For HTTP header, and src/app/api/auth/login/route.js uses that spoofable value for checkLock and recordFail. A remote attacker can rotate the X-Forwarded-For value on each login attempt to receive a fresh rate-limit bucket, bypass the 5-attempt threshold and progressive lockout durations, and perform unlimited brute-force attempts against the dashboard password. This issue is fixed in version 0.4.80.