CVE-2026-55501: 9router Login Rate-Limit Bypass via X-Forwarded-For Spoofing
The 9router dashboard login page lets attackers bypass its brute-force lockout entirely by rotating a spoofed X-Forwarded-For header on each request, giving every attempt a fresh rate-limit counter.
The problem
The login rate limiter in `src/lib/auth/loginLimiter.js` identifies clients by reading `X-Forwarded-For` directly from the incoming request headers. No trusted-proxy validation is performed, so the value is fully attacker-controlled.
Because the in-memory `Map` keys on this header value, each unique header creates an independent bucket with zero recorded failures. The 5-attempt threshold and the progressive lockout (30 s, 5 min, 30 min) are never reached. On default installations the rate-limit bypass combines with the default dashboard password to allow unauthenticated administrative access.
Proof of concept
A working proof-of-concept for CVE-2026-55501 in 9router, with the exact payload below.
# Rotate X-Forwarded-For on every request; lockout never triggers
for i in $(seq 1 100); do
curl -s -X POST "http://TARGET:20128/api/auth/login" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 10.0.0.$i" \
-d '{"password":"<password-to-brute-force"}'
echo
doneThe root cause is CWE-290 (Authentication Bypass by Spoofing): `getClientIp()` returns `xff.split(',')[0].trim()` without checking whether the request arrived from a trusted upstream proxy. Any client can set this header to any value.
The fix in 0.4.77 gates the `X-Forwarded-For` read behind a trusted-proxy list. When the TCP peer is not in that list, `getClientIp()` falls back to the socket's remote address instead, which the attacker cannot control. This ensures each attacker IP maps to exactly one rate-limit bucket regardless of what headers they send.
The fix
Upgrade to 9router 0.4.77 or later. If you cannot upgrade immediately, place 9router behind a reverse proxy configured to overwrite or strip `X-Forwarded-For` before requests reach the application. Also change the default dashboard password if it has not already been rotated.
Reported by decolua.
Related research
- critical · 9.9CVE-2026-55500CVE-2026-55500: 9router Unprotected Database Export/Import Credential Theft
- critical · 109router: Unauthenticated CRUD on Provider API and Full API Key Leak
- highCVE-2026-53823: OpenClaw Slack allowFrom Authentication Bypass via Mutable Display Names
- highCVE-2026-53832: openclaw Trusted-Proxy Identity Header Forgery via Same-Host Loopback