Web › Module 6 › Lesson 3
Race Conditions and Limit Overrun
Two requests, one limit — design must serialize the truth. Local counter only.
Visual · race_limit_overrun
Two arrows hit one counter. Limit says 1; both see 0; both write 1. DEMO http://192.168.0.1/ or SAFE Never a bank-steal race.
Opening
Races and limit overruns are timing against a rule you forgot to lock.
OWASP Top 10:2025 A06 Insecure Design includes concurrency failures: a one-time coupon checked then marked used in two steps without a transaction; a seat counter read-modify-write without locking; a “max five” limit that two parallel requests both pass. The attacker goal is to overrun a business limit. Cyberlium teaches the mechanism with a local HTTP counter YOU own on — Identify the banner first; router login is OUT OF SCOPE. Not a recipe to drain a bank, not a gift-card double-spend against a live brand. Attack literacy → own-lab → fix/report. Fix means atomic check-and-set, unique constraints, idempotency keys, and queues — named on your stack. Next is the design review lab. Today: race literacy without exploit kits.
1. Check-then-act: the gap where two truths fit
Classic pattern: read limit, decide OK, write new state. Between read and write, another request does the same. Both saw “available.” Both succeeded. The design assumed single-threaded humans. The fix is not “hope.” The fix is one atomic operation or a uniqueness constraint the second write cannot violate. You will demonstrate that gap on a toy HTTP counter under cyberlium-lab — never on a payment network.
Limit overrun is the business name for the same gap when the limit is quantity, budget, invites, or “once.” Rate limiting at the edge helps bots; it does not replace transactional integrity for money-like counters.
Command guide
A06 Design — Check-then-act
═══ INSTALL ═══
Linux (Debian/Ubuntu):
Command — copy this
sudo apt install curl
macOS: Built-in
Windows: Built-in (PowerShell: Invoke-WebRequest)
═══ COMMANDS ═══
Command — copy this
SAFE="http://127.0.0.1:8773"
Command — copy this
curl -sS -m 3 "$SAFE/counter" | head -n 12 curl -sS -m 3 -X POST "$SAFE/counter/inc" | head -n 12
2. Design controls: transactions, uniqueness, idempotency
Name the controls you would require in a design review: database transaction with row lock or serializable isolation for the counter; UNIQUE(user_id, coupon_id) so the second insert fails; Idempotency-Key on payment-like POSTs so retries do not double-apply; queue a single worker for contested resources. You do not need a vendor cookbook — you need the sentence “two concurrent applies cannot both succeed.”
Idempotency is design: the client may retry; the server must treat duplicate keys as one effect. Document where keys are stored and how long. Empty “we will add locks later” fails A06.
Command guide
A06 Design — Design controls
═══ INSTALL ═══
Linux (Debian/Ubuntu):
Command — copy this
sudo apt install curl
macOS: Built-in
Windows: Built-in (PowerShell: Invoke-WebRequest)
═══ COMMANDS ═══
Command — copy this
SAFE="http://127.0.0.1:8773"
Command — copy this
curl -sS -m 3 -I "$SAFE/" | head -n 10
3. What this course refuses: bank-steal and live double-spend PoCs
Public writeups sometimes show racing checkout on real brands. That is out of scope here. Your lab is a Python HTTP counter in $HOME/cyberlium-lab that intentionally has a check-then-act bug, then a fixed version with a lock. No card numbers. No foreign hosts. No hydra on the router. No nmap of 192.168.0.0/24. If it feels too small without real money, that feeling is how people cross into unauthorized testing.
Report shape on YOUR app: asset http://192.168.0.1/ or SAFE, steps on loopback, expected single success, actual double success, invariant, proposed atomic control. Stop.
4. Observability: log the denied second apply
When uniqueness or locks deny the second writer, log actor, resource, and outcome without secrets. A09 will deepen logging; A06 already needs the design to emit that signal so limit abuse is visible. Silent success on both writers is the insecure design.
Carry forward: races are design until proven otherwise. Implementation without locks is how the design fails in production.
5. Wrong vs right: live payment races vs local counter literacy
Same word “race,” opposite job. Right never needs a bank or a router login.
Wrong
Publish or run bank-steal / gift-card double-spend PoCs. Race a stranger checkout. Hydra 192.168.0.1. nmap the LAN. Skip atomic controls. Store session tokens in the note.
Right
Identify DEMO; if router, SAFE 127.0.0.1:8773. Explain check-then-act; run broken vs fixed local HTTP counter YOU wrote; name transaction/unique/idempotency; lock race-limit-notes.txt. Next: Lab — Design Review Note.
6. Hands-on: HTTP counter broken vs fixed (8773)
Start a06_counter_toy.py (binds 127.0.0.1:8773). GET /broken/apply (check-then-act, may overrun). GET /fixed/apply (lock). Two curls close together on broken; record. No bank. No hydra.
Command guide
race_limit_local_counter.sh — DEMO identify + HTTP counter 8773
═══ INSTALL ═══
Linux (Debian/Ubuntu):
Command — copy this
sudo apt install curl sudo apt install nmap sudo apt install python3
macOS:
Command — copy this
brew install nmap brew install python3
Windows: Built-in (PowerShell: Invoke-WebRequest)
Command — copy this
choco install nmap # or download https://nmap.org/download.html
Download https://python.org/downloads/
═══ COMMANDS ═══
Command — copy this
cd "$HOME/cyberlium-lab"
Command — copy this
cat > a06_counter_toy.py << 'PY'
"""Teaching check-then-act vs lock. Bind loopback only."""
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
import threading
import time
from urllib.parse import urlparse
HOST = "127.0.0.1" # bind only — never 0.0.0.0, never argv
PORT = 8773
if HOST != "127.0.0.1":
raise SystemExit("refusing non-loopback bind")
LIMIT = 1
broken_n = 0
fixed_n = 0
lock = threading.Lock()
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
print("[demo]", fmt % args)
def _send(self, code: int, body: str):
data = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
global broken_n, fixed_n
p = urlparse(self.path).path
if p == "/broken/apply":
n = broken_n
time.sleep(0.08)
if n < LIMIT:
broken_n = n + 1
return self._send(200, f"broken applied n={broken_n}")
return self._send(409, f"broken denied n={broken_n}")
if p == "/fixed/apply":
with lock:
if fixed_n < LIMIT:
fixed_n += 1
return self._send(200, f"fixed applied n={fixed_n}")
return self._send(409, f"fixed denied n={fixed_n}")
if p == "/reset":
broken_n = 0
fixed_n = 0
return self._send(200, "reset")
return self._send(
200,
"Cyberlium A06 counter toy. DEMO writeup: http://192.168.0.1/ "
"GET /broken/apply GET /fixed/apply GET /reset LIMIT=1",
)
print("bind", HOST, PORT)
print("demo_writeup_url http://192.168.0.1/")
print("if 192.168.0.1 is a router, curl", f"http://{HOST}:{PORT}/")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
PYCommand — copy this
cat > a06_l03_practical_commands.sh << 'SH' wait wait SH
Command — copy this
{Mission: race-limit-notes.txt + local counter (mode 600)
1) 0.0.1:8773 after starting a06_counter_toy.py.2) Define check-then-act; run broken vs fixed HTTP counter curls; name lock/unique/transaction/idempotency. 3) chmod 600 $HOME/cyberlium-lab/race-limit-notes.txt. No bank PoCs. No hydra. No nmap.
Stuck? Ask Cyberlium AI Mentor
If the lesson feels “fake” without a payment race, ask for a hint — not a merchant target. Try: "Hint only: why check-then-act overruns a limit; what UNIQUE or a lock changes; why 192.168.0.1 router login is OUT OF SCOPE; where my local counter lives?" You still use cyberlium-lab only.
Race literacy without bank-steal kits: two requests, one limit, design must serialize truth — proven with DEMO/SAFE curls on 8773. A06 Insecure Design. Original Cyberlium — not official OWASP certification. Next — Lab — Design Review Note — pull threat model, logic, and race lines into one review on an app you own.
Knowledge Check
APPLY: Two parallel requests both pass “coupon unused” then both mark used. What failed, and what is in-scope for Cyberlium?
Multiple choice
Knowledge Check
APPLY: True or False: Edge rate limits alone make one-time coupon races impossible.
True or False
Knowledge Check
APPLY: curl of http://192.168.0.1/ is a Netgear router login. Race lab next step?
Multiple choice