Video summary
Global AppSec Dublin: [T]OTPs Are Not As Secure As You Might Believe - Santiago Kantorowicz
Main summary
Key takeaways
Main ideas / lessons conveyed
- OTP/TOTP are often low-entropy challenges (typically too-short numeric codes), making them vulnerable to guessing/brute-force—especially when implementations allow:
- long validity windows,
- multiple codes being valid at once,
- repeated attempts,
- and/or parallel targeting of many users.
- “Time window” math matters:
- OTP (e.g., SMS/email codes) is typically valid for a few minutes (a fixed window).
- TOTP (authenticator-app codes) is generated using time + secret and typically has a sliding window where multiple codes from adjacent time steps can be valid.
- Real-world attack feasibility depends heavily on product/UX decisions:
- how many attempts a user can make,
- how many OTPs can be requested within the validity time,
- and whether the system “reveals” failures (or stays silent until the end).
- Common implementation bugs can further weaken security, sometimes turning “one-time” into “reusable” in practice (e.g., wrong session binding, client-side validation, legacy login flows without 2FA).
- Mitigations rarely solve the problem alone; best results come from combining controls (rate limiting, better entropy, dynamic defenses, careful messaging, and potentially stronger factors).
Concepts and terminology recap (as presented)
-
OTP (One-Time Password)
- Example described: Uber sends a 4-digit SMS OTP (digits 0–9).
- Usually:
- randomly generated,
- expires after a few minutes,
- often allowed multiple attempts during that time.
-
TOTP (Time-based One-Time Password)
- Not purely random: generated by algorithm(time, secret).
- Typically:
- 6–8 digits,
- expires after a few minutes,
- due to time-step alignment, there can be a window where more than one TOTP is valid.
-
Key security observation
- Not only “one code at a time” may be valid—implementations can allow multiple concurrent valid codes, which changes brute-force probability.
Attack scenario / story motivating the talk
- Research context at Twilio (company division working on OTP/TOTP products).
- A bug bounty report claimed: with unlimited attempts, an attacker could add an device using OTP-based verification.
- Initial reasoning suggested success chance was small (e.g., “one chance in a million” per guess), but the presenter later found the math/behavior of validity windows and repeated requests increases success probability far more than expected.
- Outcome:
- they wrote a blog post and delivered this talk,
- received recognition that it was a valid finding.
Core methodology: probability of guessing within OTP validity windows
Parameters used repeatedly in the talk (example defaults)
-
OTP length / space
- 4-digit numeric → (10^4 = 10,000) possibilities
- 6-digit numeric → (10^6 = 1,000,000) possibilities
- 8-digit numeric → implied accordingly (larger space)
-
Attempts allowed per window
- typical example uses 5 attempts (noted that many sites allow ~3–5; some allow more)
-
Validity time / windows
- OTP valid for ~3 minutes (because SMS/email delivery delays)
- within that 3-minute window, the system may allow multiple OTP requests (example limit: up to 10 valid OTPs at a time)
-
Sliding window behavior for TOTP
- typical time step is 30 seconds
- commonly valid across two time steps (~1 minute total validity window)
Single-user guessing (OTP via SMS/email)
-
4-digit OTP example
- 10,000 possibilities → 1/10,000 per guess
- allowing 5 attempts within 3 minutes increases success chance (presenter gives ~0.05% for 5 guesses, then later much higher when multiple OTPs are valid concurrently)
- with up to 10 OTPs valid in one window, success probability becomes ~0.5% (~1 in 200) for that scenario
-
Repeating across multiple windows
- the presenter corrects a misconception: even though OTPs expire, the attacker can request more codes in the next window, and the cumulative probability over time becomes large.
-
Conclusion from examples
- Numeric OTPs that are too short + enough attempts/windows lead to high success probabilities over days/months.
“One user” over longer periods
- 6-digit OTP
- presenter’s computed success probability:
- about 2.3% per day for the given assumptions
- about 91% over 100 days
- about 99% over ~6 months
- presenter’s computed success probability:
- Presenter claims these probabilities are not “made up”:
- they built a simulator to validate calculations,
- simulator results matched the derived numbers (e.g., 91, 2.3, ~99).
Why TOTP makes it worse (compared to naive expectations)
- For TOTP, the valid codes aren’t strictly isolated to one exact 30-second bucket; because of implementation windows:
- multiple TOTP values can be valid in overlapping time steps,
- brute forcing must account for how attempts distribute between time steps.
- Presented outcome:
- TOTP can be more favorable to attackers if the system uses permissive windowing and allows sufficient attempts/requests.
Parallelizing the attack (credential stuffing + OTP guessing)
Why attacking many users at once is critical
- If an attacker already has correct user credentials (from password reuse/credential stuffing), they only need to guess the OTP/TOTP as the second factor.
- Presenter’s framing:
- If OTP is a single step that grants access once guessed correctly, then attacking in parallel across many accounts dramatically increases total chances.
Multi-user success examples (as presented)
-
6-digit OTP
- Attack 100 users in parallel → presenter suggests ~91% chance of success in one day (under their assumptions).
-
8-digit OTP
- Even with fewer digits expanded to 8-digit, the presenter claims parallel attacks still allow high success probability over feasible time horizons (e.g., “in 100 days” style outcomes).
-
Rate limiting alone doesn’t fully solve it
- Example rate limit used: 60 OTPs/month (i.e., ~2/day)
- Presenter claims that with sufficiently parallel targeting, attackers still achieve high probability of at least one success within about a month (for large user counts like 1,000+ users).
Alphanumeric codes: partial improvement, but tradeoffs
- Presenter argues that moving from numeric-only to alphanumeric increases the search space and reduces success probability.
- However:
- it may cause user friction and raise support/UX issues.
- They explicitly caution:
- “Don’t just switch to alphanumeric OTPs” and assume it fixes everything.
Implementation/design pitfalls highlighted (beyond brute-force math)
The talk includes examples of common engineering mistakes that can undermine OTP security even further, such as:
- Flow bypass / insufficient session binding
- System checks the OTP is valid but doesn’t bind it to:
- the correct user, or
- the correct session/context.
- System checks the OTP is valid but doesn’t bind it to:
- Client-side comparisons
- Putting OTP checks on the client (or storing expected OTP as hidden client-side values).
- Legacy login routes without 2FA
- New login requires 2FA, but older login endpoints still exist and don’t require 2FA.
- Reusing OTPs
- OTPs that should expire or be single-use still work when reused.
- Insufficient entropy / digit length
- Examples include too-short OTPs (e.g., customer wanting 3 digits) leading to “very risky” outcomes.
Mitigations proposed (with actionable bullet points)
Presenter states there is no single mitigation that fully works, but suggests layered defenses:
1) Cryptographic correctness / consistency in delivery
- Deliver the same OTP for the same validity window
- Particularly for SMS/email delivery (avoid “rotating” multiple times unpredictably in a way attackers can exploit).
- Encrypt OTP generation/delivery state server-side
- Example idea: protect secrets using HMAC or equivalent.
- Ensure the server must verify against protected values, not client-side checks.
- Regenerate OTP only when genuinely required
- Create a new OTP when a new request is made, but keep consistency rules well-defined.
2) Rate limiting and attempt controls (granular)
- Limit attempts per user
- Per day / per week / per month granularity.
- Avoid extremely permissive “global” limits
- If you allow too much, attackers can parallelize.
- Use incremental failure delays
- After failed attempts, introduce progressive delays rather than allowing many rapid tries.
- Reset attempt counters on successful login
- Reduce lock-in friction for real users.
3) Detect automated abuse
- IP-based and endpoint rate limits
- Attack traffic often concentrates on the same endpoints from fewer IPs unless attackers use large bot infrastructure.
- Detect repeated failures / patterns
- Monitor unusual OTP request volume and failure rates.
4) Increase entropy dynamically or adapt under attack
- Dynamically increase OTP size
- e.g., escalate from 6 digits numeric to more characters when suspicious activity is detected.
- Switch from numeric to alphanumeric under attack
- Add step-up challenges
- Captchas (not great UX, but can help during active attack).
- Knowledge questions (sucks UX, but can help).
- Multi-channel verification
- If attacked, send OTP via another channel:
- SMS → email OTP, or email link with much higher entropy than digits.
- Keep user messaging non-alarming while still secure.
- If attacked, send OTP via another channel:
5) Don’t leak which part was wrong
- Avoid disclosing whether OTP or password is wrong during the flow
- Prevent attackers from learning information needed to optimize credential stuffing.
- (Idea repeated in Q&A: don’t reveal intermediate failure; answer after full input.)
6) Strengthen authentication factors beyond OTP (strategic options)
- U2F / hardware security keys
- Stronger, but may not be feasible due to user adoption/churn.
- Push authentication
- Alternative factor with different UX and risk properties.
- Direct carrier billing / “recovery billing”
- Mentioned as a mechanism to charge/authenticate via mobile carrier billing (presented as low friction in supported countries, but not guaranteed secure; the presenter didn’t verify deeply).
- Service-level policy
- Consider step-up to stronger factors for high-risk users (e.g., high-value accounts).
7) Security validation and sizing the risk
- Do the math for your own parameters
- Assumptions change with:
- window size,
- number of OTPs valid at once,
- allowed attempts,
- and concurrency/rate limiting.
- Assumptions change with:
Q&A highlights (additional constraints / clarifications)
- “If you don’t disclose whether OTP/password is wrong, is credential stuffing harder?”
- Presenter agrees it helps, but suggests user/UX and risk tradeoffs.
- “Does reducing attempts from 5 to 3 improve much?”
- Presenter claims limited impact in their model; attackers succeed over time mostly due to windows and concurrency.
- “Have you tested on real systems?”
- Presenter did not provide a full comparative survey of Google/Microsoft/etc. settings, and says behaviors vary; attackers can exploit many real systems with permissive behaviors.
- “What about backup codes?”
- Presenter didn’t provide math for backup codes; notes backups often exist and must be designed with the same risk mindset. In many systems backups still lead back to SMS OTP risk.
Speaker/source list
- Santiago Kantorowicz (presenter; works at Twilio; security-related role connected to OTP/TOTP products)
- Audience participants / questioners (unnamed; asked multiple questions during the session)
- Uber (example OTP usage and screenshot reference)
- Authy, Google Authenticator, RSA token app (examples of TOTP/token usage)
- Citibank (example mentioned re: OTP digit-length change)
- Bug bounty researcher / report author (referenced as the source of the initial unlimited-attempts finding; not named)