SourCode a FreeFungi app

Challenges FF-007

Sherbet CWE-307 A07:2021

Gift Card Grinder

No rate limit on code redemption.

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:

✗ Vulnerable
app.post('/gift-card',
  requireAuth,
  redeem);
✓ Patched
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.