agentggagentgg
Back to all findings
CRITICALconfirmedweak-password-resetrate-limit-bypass229d2c853bf8

Reset-password rate limit trivially bypassed via spoofed X-Forwarded-For

The express-rate-limit middleware on /rest/user/reset-password keys requests off the client-controlled X-Forwarded-For header, so an attacker can rotate that header per request and brute-force security answers without ever hitting the 100/5-minute cap.

Fileserver.ts
Lines315319
Confidence
80%
File statusvalidated
Details

Lines 315–319:

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 }
}))

app.enable('trust proxy') is set unconditionally on line 314, and the keyGenerator reads headers['X-Forwarded-For'] directly. Since this header is supplied by the client, an attacker who sends a different X-Forwarded-For value on each request gets a fresh bucket every time — the limiter is effectively disabled for anyone aware of the header. Header lookup is also case-sensitive in Node (req.headers is lower-cased), so the literal key 'X-Forwarded-For' will be undefined for any honestly-proxied request and silently fall back to ip, while an attacker can set the exact-case header via raw HTTP to dominate the key space.

Given the reset flow (per Juice-Shop and the imported securityQuestion/SecurityAnswer models) gates password reset on a security-answer knowledge factor, the absence of an effective rate limit/lockout (criterion 4 in the brief) directly enables online brute-force of those low-entropy answers.

Proof of concept

Rotate the spoofed header per request — each one creates a new rate-limit bucket

for i in $(seq 1 100000); do curl -s -X POST https://target/rest/user/reset-password \ -H "X-Forwarded-For: 10.0.$((i/256)).$((i%256))" \ -H 'Content-Type: application/json' \ -d "{\"email\":\"victim@example.com\",\"answer\":\"guess$i\",\"new\":\"x\",\"repeat\":\"x\"}" done

Impact

Unauthenticated attackers can brute-force security answers (or reset tokens) for any user at unlimited throughput, defeating the only quantitative defence the application places in front of the security-question-based reset flow. Leads to full account takeover.

Validation
confirmed

The keyGenerator at lines 315-319 returns headers['X-Forwarded-For'] ?? ip. Because app.enable('trust proxy') is set on line 314, Express's req.ip is itself derived from the (attacker-controlled) X-Forwarded-For header, so even when the case-sensitive headers['X-Forwarded-For'] lookup falls back to ip, the rate-limit key is still client-controllable — rotating the XFF header per request yields fresh buckets. The reset-password route (app.post('/rest/user/reset-password', utils.asyncHandler(resetPassword()))) is the only quantitative defense in front of the security-answer–gated reset flow, so the bypass directly enables online brute-force of low-entropy answers. The PoC shape (POST with email/answer/new/repeat JSON) matches Juice-Shop's reset endpoint.

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 /rest/user/reset-password endpoint is mounted at line ~559 with no auth middleware in front of it, and the rate limiter at lines 315–319 keys off headers['X-Forwarded-For'] — a fully client-controlled header — so any remote, unauthenticated attacker can rotate the header per request to brute-force low-entropy security answers for arbitrary users (network-reachable, no prep, no victim interaction). Because the brute force can target any account (including admin), the worst plausible outcome is full takeover of arbitrary accounts via the password-reset sink, yielding total disclosure (C:H), the ability to overwrite the victim's password (I:H), and effective denial of access to the legitimate owner once their password is changed (A:H). Impact stays within the Juice-Shop application authority, so Scope is Unchanged. The case-sensitive 'X-Forwarded-For' lookup against Node's lower-cased req.headers only further weakens the limiter and does not raise complexity for the attacker, who sends raw HTTP with the exact-case header.

References