CWE-307 is the absence of a brake. The product does not take sufficient measures to prevent multiple failed authentication attempts within a short time frame, so an attacker can keep trying.
Login forms, APIs, SSH and database listeners, device admin interfaces and password reset flows are all candidates. Anything that accepts a secret and answers yes or no is a target.
How it happens
The weakness is rarely one dramatic bug. It is a series of small omissions:
- The login endpoint answers every request at full speed, forever.
- Failed attempts are counted per session instead of per account, so an attacker simply starts a new session.
- The limit exists on the web form but not on the REST or mobile API that uses the same credentials.
- Connections are never closed after repeated failures.
- A limit exists but is very high.
MITRE's observed CVEs show these variations:
- CVE-2019-0039: the REST API of a network operating system had a high limit on the number of connections, allowing brute force password guessing.
- CVE-1999-1152, CVE-2001-1291 and CVE-2001-0395: products that did not disconnect or time out after multiple failed logins.
# Vulnerable: no counting, no delay, no lockout
@app.route("/login", methods=["POST"])
def login():
user = find_user(request.form["email"])
if user and check_password(user, request.form["password"]):
return start_session(user)
return "Invalid credentials", 401
What an attacker gains
An attacker can make an arbitrary number of attempts with different passwords and eventually gain access to the targeted account by brute force. MITRE lists the impact as bypass of a protection mechanism, in the sense that the guessing limit that should protect the secret is not there.
How to find it
MITRE's detection notes, grouped:
- Dynamic analysis with automated interpretation. Web application scanners, web services scanners and database scanners are rated highly cost effective. Host-based vulnerability scanners give partial coverage.
- Dynamic analysis with manual interpretation. Fuzz testers and framework-based fuzzers are rated highly cost effective.
- Manual source review. A focused spot check of the authentication code is rated highly cost effective.
- Automated static analysis of source code gives partial coverage.
An easy manual test: send twenty or fifty wrong passwords for one account in a row, then the right one. Compare response times and messages at attempt one and attempt fifty. If nothing changes, nothing is protecting you. Repeat against each interface that accepts credentials.
How to fix it
MITRE lists common protection mechanisms:
- Disconnect the user after a small number of failed attempts.
- Implement a timeout.
- Lock out the targeted account.
- Require a computational task on the user's part.
Apply them with some care:
- Count failures per account and per source, not only per session.
- Use escalating delays rather than permanent lockouts alone, since a hard lockout lets an attacker deliberately lock out real users.
- Cover every interface that checks the same credential: web, API, mobile backend, admin console.
- Prefer a vetted library or framework with authentication support. MITRE names OpenSSL and the ESAPI Authenticator as examples.
- Log failures and alert on bursts so the attempt is noticed.
# Better: throttle by account, delay grows with failures
def login():
user = find_user(request.form["email"])
if too_many_recent_failures(user, limit=5, window_minutes=15):
return "Try again later", 429
if user and check_password(user, request.form["password"]):
reset_failures(user)
return start_session(user)
record_failure(user)
return "Invalid credentials", 401
Treat this as one layer. Multi-factor authentication (see CWE-308) makes a guessed password far less useful.
Where the limits usually go missing
Most teams do add a limit to the main login page. The gaps appear around it.
Password reset and "forgot username" flows often answer yes or no for each guess with no throttle. Mobile and partner APIs sit behind a different gateway than the web form. Legacy admin interfaces, database listeners and device consoles may have been deployed with defaults and never revisited. Service accounts and API keys often have no failure counting at all, because nobody expects a human to type them.
A useful exercise is to list every place your product accepts a secret and fill in a small table: what is the limit, what happens at the limit, and who gets alerted. Blank cells are your findings. It takes an hour and usually turns up at least one interface that treats the thousandth wrong guess exactly like the first.
Try it yourself
This demo walks through a dynamic web application scan, one of the detection methods MITRE rates highly for this weakness.
Click through it at your own pace, or open it full screen.
Quick check
Is a CAPTCHA enough?
It helps, but treat it as one control. MITRE's list also includes disconnecting, timeouts and account lockout, and they work better together.
Does rate limiting the login page cover the API?
Only if the API shares the limit. Each interface that accepts credentials needs its own protection, or a shared one.