ASVS 5.0 V6.6.3 requires code based out of band authentication to be protected against brute force with rate limiting. It also suggests considering codes with at least 64 bits of entropy. This is a Level 2 requirement.
Why it matters
A six digit code has a million values. At a hundred requests per second, an attacker covers that in under three hours, and a lucky guess comes much sooner. Without limits, the short life of the code does not save you, because attackers can request fresh codes and keep trying.
How to implement it
Limit attempts per code, per account and per source. Count failures server side, in shared storage so all instances agree.
import redis
r = redis.Redis()
MAX_PER_CODE = 5
MAX_PER_ACCOUNT_HOUR = 20
def guard(attempt_id: str, user_id: str):
per_code = r.incr(f"oob:code:{attempt_id}")
r.expire(f"oob:code:{attempt_id}", 600)
per_acct = r.incr(f"oob:acct:{user_id}")
r.expire(f"oob:acct:{user_id}", 3600)
if per_code > MAX_PER_CODE or per_acct > MAX_PER_ACCOUNT_HOUR:
raise TooManyAttempts() # HTTP 429, and invalidate the code
def on_failure(attempt_id):
if r.get(f"oob:code:{attempt_id}") and int(r.get(f"oob:code:{attempt_id}")) >= MAX_PER_CODE:
db.attempts.invalidate(attempt_id)
After the cap, kill the code so the attacker must start over and trigger a new message. Also limit how many codes can be requested per hour. Use longer codes (64 bits is about 20 digits or 11 characters of base32) for mechanisms where limits are hard to enforce, such as links.
Remember that the response must not reveal whether the account exists (V6.3.8) and that errors must look the same under limiting.
How to test it
- Submit wrong codes in a loop and confirm lockout or HTTP 429 after your limit, not after hundreds.
- Confirm the correct code no longer works once the attempt cap has been hit.
- Request codes repeatedly and confirm a request cap.
- Rotate source IPs but keep the same account and confirm the account limit still applies.
Common mistakes
- Limiting by IP only, which bots bypass with proxies.
- Counters in server memory, which reset on restart or differ across instances.
- A limit that resets when a new code is issued.
- Returning the remaining attempts, which helps the attacker plan.
Try it yourself
API scanning shows whether an endpoint keeps accepting guesses without throttling.
Click through it at your own pace, or open it full screen.
Quick check
Is IP based rate limiting enough?
No. Attackers spread guesses across many addresses. Limit per code and per account as well.
What should happen to a code after too many wrong guesses?
It should be invalidated, so further guesses fail even if one happens to be right.