Accepted risks
Accepted risks are residual risks the team documents and moves past, usually because the mitigation lives outside this service or because the business has decided the tradeoff is worth it.
SIM swapping, SS7 interception, and compromised email inboxes are all attacks on how the OTP gets to the user in the first place (i.e. delivery-channel attacks). The team heard the security guidance that SMS is no longer considered a strong authentication factor, and they agreed with it, but they decided to ship SMS anyway because they wanted to start with lower-friction options to keep the user experience smooth. This kind of tradeoff is a routine part of AppSec work, so to make it defensible the team documented this decision, made sure org leaders were aware of the risk, and committed to revisit it from time to time.
A verifier written in any language can’t defend against an attacker who already controls the channel, and the team decided that was a tradeoff they were willing to make. A related problem is SMS bombing: an attacker hits the issuance endpoint over and over to run up the SMS bill and spam the targeted user with codes they didn’t ask for. Fixing that is a product and provider job, not something this code can solve.
The other risk the team accepted is this service restarting. The rate limiter keeps its counts in memory, so when the service goes down and comes back up, the counts go back to zero. An attacker mid-brute-force would get a fresh five attempts to keep guessing, but the timing has to line up just right and the codes only stay valid for a few minutes anyway. The team could have used Redis to keep the counts across restarts, but the platform team asks application teams to keep their Redis footprint small. A rare, complex attack that buys the attacker a handful of extra guesses against a single short-lived code was not worth that budget.
If the team wanted to scale this service horizontally, meaning they ran more than one instance of it at the same time, they’d hit a similar problem. Each instance would count guesses on its own, so an attacker could spread their guesses across instances and stay under the limit on any one of them. For now they only run one instance, which sidesteps the problem entirely. If they scale up to more than one, they’ll need to revisit the limiter design.
| ID | Risk | ROAM | STRIDE | Notes |
|---|---|---|---|---|
OTP-6 | Email account compromise | Accepted | Spoofing | Attacker controls the delivery channel and receives codes intended for the legitimate user |
OTP-2 | Issuance flooding / SMS bombing | Accepted | Denial of Service | Exhausts SMS budget and disrupts the user's device with unsolicited codes |
OTP-11 | Multi-replica state divergence | Accepted | Tampering | Routing requests across replicas circumvents the rate limit counter on any single replica |
OTP-10 | Service restart wipes rate limit state | Accepted | Tampering | The rate limit counter is effectively reset by an out-of-band action, weakening the control |
OTP-4 | SIM swapping | Accepted | Spoofing | Attacker takes control of the delivery channel to receive codes intended for the legitimate user |
OTP-5 | SS7 interception | Accepted | Information Disclosure | Code is exposed to a network-level adversary in transit |
No risks match the current filters.
Owned risks to act on
Owned risks are the ones the team has to address as they implement this service.
Spoofing is the most common category of risks, since it covers most of what an attacker is actually trying to do: authenticate as the real user without controlling the delivery channel (i.e. email, SMS). Online guessing against the six-digit code is the obvious risk, so the team will implement a per-user rate limit with a hard cap on attempts within the validity window. Weak code generation is another spoofing path: if the service derives codes from timestamps, counters, or a non-cryptographic PRNG, an attacker may be able to predict the next code without exhausting the rate limit at all. Issuance has to use a CSPRNG.
The team also has to protect against attacks when an OTP is used successfully. If they don’t mark it used and reject it on the next try, an attacker who intercepts a valid OTP can submit it again and bypass MFA. They also need to make sure an OTP issued for one user can’t be accepted for a different one. Both are cases of Elevation of Privilege, since the attacker would end up in an account that was never theirs.
Finally, a timing attack constitutes Information Disclosure: when verifying an OTP, a comparison that short-circuits on the first wrong byte leaks how much of the guess was correct. So, the internal verification API call needs to be constant-time.
| ID | Risk | ROAM | STRIDE | Notes |
|---|---|---|---|---|
OTP-3 | Code prediction / weak RNG | Owned | Spoofing | Predictable codes let an attacker derive a valid second factor without observing it |
OTP-9 | Cross-user code confusion | Owned | Spoofing Elevation of Privilege | A code valid for one user is accepted for another, granting access to a different account |
OTP-1 | Online brute force of the code | Owned | Spoofing | Attacker impersonates the legitimate user by guessing their second factor |
OTP-8 | Replay of consumed codes | Owned | Spoofing Elevation of Privilege | An already-used code is presented again to authenticate a second time |
OTP-7 | Timing attacks on code comparison | Owned | Information Disclosure | Response latency leaks information about correct prefixes of the code |
No risks match the current filters.
Full register
Every enumerated risk with ROAM and STRIDE metadata.
| ID | Risk | ROAM | STRIDE | Notes |
|---|---|---|---|---|
OTP-3 | Code prediction / weak RNG | Owned | Spoofing | Predictable codes let an attacker derive a valid second factor without observing it |
OTP-9 | Cross-user code confusion | Owned | Spoofing Elevation of Privilege | A code valid for one user is accepted for another, granting access to a different account |
OTP-1 | Online brute force of the code | Owned | Spoofing | Attacker impersonates the legitimate user by guessing their second factor |
OTP-8 | Replay of consumed codes | Owned | Spoofing Elevation of Privilege | An already-used code is presented again to authenticate a second time |
OTP-7 | Timing attacks on code comparison | Owned | Information Disclosure | Response latency leaks information about correct prefixes of the code |
OTP-6 | Email account compromise | Accepted | Spoofing | Attacker controls the delivery channel and receives codes intended for the legitimate user |
OTP-2 | Issuance flooding / SMS bombing | Accepted | Denial of Service | Exhausts SMS budget and disrupts the user's device with unsolicited codes |
OTP-11 | Multi-replica state divergence | Accepted | Tampering | Routing requests across replicas circumvents the rate limit counter on any single replica |
OTP-10 | Service restart wipes rate limit state | Accepted | Tampering | The rate limit counter is effectively reset by an out-of-band action, weakening the control |
OTP-4 | SIM swapping | Accepted | Spoofing | Attacker takes control of the delivery channel to receive codes intended for the legitimate user |
OTP-5 | SS7 interception | Accepted | Information Disclosure | Code is exposed to a network-level adversary in transit |
No risks match the current filters.