# Throttle key bypass via input variation

Rate limits keyed on raw email, spoofable IP headers, or array params that vary per account

_Area: Web authentication and account takeover. Seen in 2 disclosed reports; one confirmed bounty: $3,500; latest 2024-01-17. Updated 2026-10-23._

## How it works
A throttle only works if every attempt against one account maps to the same counter. The weakness appears when the throttle key is derived from a user-supplied value that the authentication backend normalizes differently, or that the attacker can vary freely. The throttle layer and the auth layer disagree on identity: the limiter sees many distinct keys, the backend resolves them all to one account. The same failure occurs when the key is an attacker-controlled header. A related batching flaw lets one request test many guesses at once, sidestepping any per-request counter.

## Where it shows up
- Login/reset throttles keyed on the raw email or username string, with no canonicalization before keying [E1](https://bugbountylibrary.com/r/h1%3A1363672.md).
- Client IP taken from `X-Forwarded-For` or similar headers when a proxy is trusted [E3](https://bugbountylibrary.com/r/h1%3A2230915.md).
- Controllers passing `params` straight into `find_by`/lookups, where array params become `IN()` queries [E2](https://bugbountylibrary.com/r/h1%3A1559262.md).
- Throttle counters incremented only on failure, not on every attempt [E4](https://bugbountylibrary.com/r/h1%3A708013.md).

## How to review
The invariant: for a given account, every authentication attempt must increment the same counter, and one request must test at most one guess. Check that the throttle key is computed from the canonicalized identifier the backend actually uses (trimmed, unicode-normalized, case-folded), not the raw input [E1](https://bugbountylibrary.com/r/h1%3A1363672.md). Check that IP keys come from the real peer address, not proxy headers, unless the proxy is fully trusted and strips client-supplied values [E3](https://bugbountylibrary.com/r/h1%3A2230915.md). Check that array-typed params are rejected or cast to scalar before any lookup [E2](https://bugbountylibrary.com/r/h1%3A1559262.md). Confirm the counter increments on success and failure alike [E4](https://bugbountylibrary.com/r/h1%3A708013.md).

## Variants seen
- Trailing whitespace on email creates a new throttle bucket while still resolving the same account [E1](https://bugbountylibrary.com/r/h1%3A1363672.md).
- Array param `token[]` turns a single lookup into a batched oracle [E2](https://bugbountylibrary.com/r/h1%3A1559262.md).
- Spoofable `X-Forwarded-For` behind a trusted proxy [E3](https://bugbountylibrary.com/r/h1%3A2230915.md).
- Throttle counting only failed attempts [E4](https://bugbountylibrary.com/r/h1%3A708013.md).

## Why fixes fail
E1 is a bypass of the fix for E4: the earlier fix throttled failures but keyed on raw email, so normalization gaps remained. Fixes that patch one input shape leave sibling shapes open.

## Impact and severity
Account takeover via brute force of passwords or reset tokens [E1](https://bugbountylibrary.com/r/h1%3A1363672.md)[E2](https://bugbountylibrary.com/r/h1%3A1559262.md)[E4](https://bugbountylibrary.com/r/h1%3A708013.md). Severity is high when the target is a login or reset flow and the guess space is small or unlimited; worse when one request can test many guesses at once [E2](https://bugbountylibrary.com/r/h1%3A1559262.md).

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A1363672.md
- E2: https://bugbountylibrary.com/r/h1%3A1559262.md
- E3: https://bugbountylibrary.com/r/h1%3A2230915.md
- E4: https://bugbountylibrary.com/r/h1%3A708013.md
