Treat password reset and one time passcode endpoints as sensitive assets with their own limits. Cap attempts per account and per code, throttle sends per user, and watch what each send costs. A general per-address rate limit is not enough.
Why these endpoints are special
OWASP's broken authentication entry says that authentication endpoints and password reset flows should be protected like any other sensitive asset. It also says an API is vulnerable if it permits credential stuffing or brute force without captcha or lockout. Resets are an attractive route because they often have weaker checks than login.
Two things can go wrong. Attackers can guess codes, and attackers can trigger sends. The first leads to account takeover. The second leads to cost and harassment.
Guessing: limit attempts, not just requests
A six digit code has a million possibilities. If an attacker can try them freely, the code is not much of a secret.
- Limit attempts per code. After a small number of wrong tries, invalidate the code and require a new one.
- Limit attempts per account across all codes in a window.
- Make codes short lived.
- Do not let an attacker bypass the limit by changing address. Count against the account, not just the source.
OWASP's improper inventory page gives a real shaped example: a social network enforced rate limiting on reset tokens on its main API, but a beta host running the same API lacked the control, and a researcher brute-forced six digit tokens to reset any user's password. Apply limits to every host that serves the endpoint. See the post on retiring old API versions.
Triggering: limit sends
OWASP's resource consumption page describes an attacker repeatedly triggering SMS resets so that the backend sends tens of thousands of paid messages within minutes.
- Throttle sends per user and per destination number or address.
- Cap total sends per hour system wide, and alert on the cap.
- Set a spending limit or billing alert with the messaging provider.
- Return the same response whether or not the account exists, to avoid revealing which accounts are real.
Batching can bypass your limit
OWASP gives a case where a login mutation was limited to three requests per minute, but a batched GraphQL request carried many attempts at once. Count operations, not HTTP requests. See how batching defeats rate limits.
Do not forget the follow-on step
OWASP also describes an account update endpoint that let a user change email with only a bearer token. An attacker with a stolen token changed the email, then used reset to take the account. Require re-authentication for sensitive changes. More in the re-authentication guide.
A short implementation list
- Per-code attempt cap.
- Per-account attempt window.
- Per-user and per-destination send throttle.
- Global send cap with alerting.
- Provider spending limit.
- Uniform responses.
- Tests that confirm each limit fires.
Quick check
Is a captcha enough?
It helps, and OWASP lists it alongside lockout, but it does not replace per-account attempt caps.
Should locked accounts stay locked?
Be careful: a hard lockout can be used to lock real users out. Prefer short, escalating delays or code invalidation.
Safeguard's compliance and evidence automation can record that such controls exist, and its free scan at gold.safeguard.sh/scan covers dependency issues. The throttles themselves live in your code or gateway.
Sources
- OWASP API2:2023 Broken Authentication
- OWASP API4:2023 Unrestricted Resource Consumption
- OWASP API9:2023 Improper Inventory Management
Last reviewed: 19 October 2026.