agentggagentgg
Back to all findings
CRITICALconfirmedrate-limit-bypassrate-limit-bypassa2b646e60343

Reset-password rate limit keyed on attacker-controlled X-Forwarded-For header

The only rate limit protecting `POST /rest/user/reset-password` derives its key from the `X-Forwarded-For` request header, which the client controls — an attacker can rotate the header per request and trivially exceed the 100-request window.

Fileserver.ts
Lines340344
Confidence
90%
File statusvalidated
Details

Tracing from the candidate routes/resetPassword.ts (the password-reset handler with no in-file rate limiting) to its registration in server.ts, the wrapping middleware is:

app.enable('trust proxy')
app.use('/rest/user/reset-password', rateLimit({
  windowMs: 5 * 60 * 1000,
  max: 100,
  keyGenerator ({ headers, ip }: { headers: any, ip: any }) { return headers['X-Forwarded-For'] ?? ip }
}))

The keyGenerator reads X-Forwarded-For from the raw request headers and uses it as the bucket key. There is no edge layer documented as stripping or rewriting X-Forwarded-For (the project ships as a stand-alone Node app), and trust proxy only affects req.ip, not the raw header. Per the brief's true-positive criterion #2, a rate limit keyed on a spoofable header is bypassable: an attacker can set X-Forwarded-For: <random> on every request and never collide with their previous keys.

For a password-reset endpoint this is critical — it eliminates the only barrier to brute-forcing the security-answer comparison in routes/resetPassword.ts:41 (security.hmac(answer) === data.answer).

Proof of concept
import requests, uuid
for i in range(1_000_000):
    requests.post(
        'https://target/rest/user/reset-password',
        json={'email':'victim@example.com','answer':GUESS,'new':'x','repeat':'x'},
        headers={'X-Forwarded-For': str(uuid.uuid4())},
    )

Each request gets a fresh X-Forwarded-For value → a new rate-limit key → the 100-per-5-min cap never triggers.

Impact

Brute-forcing the security-answer in the password-reset endpoint, hijacking accounts (including well-known fixed answers in this codebase). More generally, the in-place rate limit is purely cosmetic.

Validation
confirmed

The keyGenerator at the cited lines returns headers['X-Forwarded-For'] ?? ip. Even though Node lowercases headers (making the first branch effectively dead) the fallback ip is itself derived from X-Forwarded-For because app.enable('trust proxy') is set immediately above, so rotating X-Forwarded-For per request still produces a fresh bucket and bypasses the 100/5min cap. The endpoint app.post('/rest/user/reset-password', ...) has no auth middleware and feeds directly into a brute-forceable security-answer comparison, so the PoC is reachable. This matches a known intentional Juice Shop vulnerability and the reported bypass works in practice.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Base score: 9.8 · CRITICAL

The keyGenerator at server.ts:340–344 keys the rate limit on headers['X-Forwarded-For'], a fully client-controlled header, and POST /rest/user/reset-password is reachable pre-auth over HTTP (AV:N, PR:N, UI:N). Rotating the header per request (as in the PoC) is a trivial, deterministic bypass with no preconditions (AC:L), enabling unbounded brute-force of the HMAC'd security answer in routes/resetPassword.ts. Successful brute-force yields full account takeover of any victim — including high-value/admin accounts with weak well-known answers — giving the attacker read of all account data (C:H), the ability to overwrite the password and account state (I:H), and lockout of the legitimate user (A:H), all within the same application authority (S:U).

References