• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Preview — this article is scheduled for publication on Apr 9, 2026. It is not listed on the blog or indexed by search engines.
Back to Blog

Apr 9, 2026 · 7 min read

The OTP Hall of Shame: Real-World Verification Bypasses That Keep Happening

OTP verification feels like a lock on your front door. But across Zomato, Grab, MTN, Shopify, and dozens of others, researchers keep walking right through it. Here are the patterns and how to avoid them.

FFlowpatrol Team·Case Study
The OTP Hall of Shame: Real-World Verification Bypasses That Keep Happening

OTP verification: the lock everyone forgets how to close

You added OTP to your sign-up flow. Six digits. A text message. A short countdown timer. It feels like a lock. The person entering the code must have access to their phone or email. Verification complete. Secure.

Except the same companies keep leaving the door unlocked in a different spot. MTN, Grab, Zomato, Shopify, HackerOne — all implemented OTP. All found ways to break it. Same mistakes. Same patterns. Different companies.

Here are the real cases, publicly reported and confirmed on HackerOne and bug bounty platforms. Study these patterns because your app probably has one of them.


Pattern 1: The OTP is in the response

This one is the most absurd, and it keeps happening.

MTN Group — one of Africa's largest telecom providers — had an endpoint that sent an OTP to verify subscriber identity before allowing subscription changes. Standard flow. Except the API response that triggered the OTP also contained the OTP itself. An attacker didn't need the victim's phone. They just read the HTTP response.

This wasn't a one-off. HackerOne report #777957 documented the subscription bypass. Reports #2633888 and #2635315 found the identical pattern on different MTN domains months later. Same company, same mistake, three times.

The code was simple:

const otp = generateOTP();
await sendSMS(phoneNumber, `Your code is ${otp}`);

return res.json({
  status: "otp_sent",
  otp: otp,  // still there — never removed
  message: "Check your phone"
});

This wasn't a subtle oversight. The OTP was returned directly in the API response. Someone added it during development for testing. It never got removed. The SMS became decoration. The real code was always visible to anyone who made the request.

Diagram showing an OTP code traveling two paths: one to the user's phone and one leaked directly in the API response
Diagram showing an OTP code traveling two paths: one to the user's phone and one leaked directly in the API response


Pattern 2: No rate limiting on verification

The WordPress Digits plugin provides SMS-based OTP login for WordPress sites. Before version 8.4.6.1, it had zero rate limiting on OTP verification attempts.

A 4-digit OTP has 10,000 possible values. At 1,000 requests per second — trivial for a single machine — that's 10 seconds to crack. Six digits? About 15 minutes. This was assigned CVE-2025-4094 with a CVSS score of 9.8 (Critical). A public exploit appeared on Exploit-DB using parallel threads to speed up the brute force.

The OpenCTI threat intelligence platform had the same problem. Its otpLogin mutation accepted unlimited OTP attempts with no lockout. An attacker with stolen credentials could blow through 2FA in minutes. Their own GitHub security advisory confirmed the issue.

Then there's Courier on HackerOne (report #1067533). They had rate limiting — but it keyed on IP address via the X-Forwarded-For header. Set that header to 127.0.0.1, and the limit reset. One header, unlimited attempts.

# Bypass IP-based rate limiting
curl -X POST https://target.com/api/verify-otp \
  -H "X-Forwarded-For: 127.0.0.1" \
  -d '{"code": "483291"}'

Pattern 3: Client-side validation

Zomato, India's largest food delivery platform, had OTP verification on its restaurant registration flow at business.zomato.com. When you entered a wrong OTP, the server returned:

{"status": "failed", "message": "Invalid OTP. Please try again"}

A researcher intercepted this with Burp Suite and changed it to:

{"status": "success", "message": "Verification successful"}

The app accepted it. Phone number verified. The OTP check existed only on the client side — the browser read "success" and moved forward. The server never enforced the decision. This was reported on HackerOne and rated high severity.

Zomato had a separate, older vulnerability too. Report #142221 showed that entering a wrong OTP while placing an order leaked the real code in the error message. Two different OTP bugs on the same platform, years apart, both rooted in the same mistake: the server wasn't the authority.

Diagram showing an attacker intercepting and modifying an OTP verification response from failed to success
Diagram showing an attacker intercepting and modifying an OTP verification response from failed to success


Pattern 4: Session issued before verification completes

Shopify had a subtle variant. When two-step verification was enabled, Shopify asked for a password first, then prompted for the OTP. A researcher found you could bypass the password step entirely and jump straight to OTP setup (report #124845). The authentication sequence wasn't enforced as a strict chain.

HackerOne themselves had a related issue (report #2529780). The platform issued a valid session token before MFA was completed. The only thing stopping the user from being fully authenticated was a client-side cookie — easily removed.

As HackerOne's own blog post put it: "A valid session token was sent to the user before the MFA code was validated. The user was only stopped from being authenticated by a client-side cookie."

If your app sets a session after password verification but before OTP verification, the OTP protects nothing. Sessions should only begin after every factor is verified.


Pattern 5: OTPs with long expiry windows

Grab, the Southeast Asian ride-hailing giant, used SMS OTP as the sole login factor for its Android app. A researcher bypassed it entirely on the login endpoint (report #205000). Since OTP was the only auth factor, the bypass meant full account takeover — ride history, payment methods, personal data.

Mars (the consumer goods company) had an OTP bypass in their password reset flow (report #3228888) that let attackers take over accounts without touching the victim's phone.

The common thread: expiry windows that are too long, and codes that don't invalidate after failed attempts.

Here's the math. A 6-digit OTP has 1 million possible values. If the code is valid for 30 minutes and doesn't invalidate after failed attempts, an attacker has 1,800 seconds to brute force it. At 100 attempts per second — trivial for an automated script — that's 18 seconds to find the code. The victim never gets a notification. The compromise happens in silence.

Timeline showing how a non-expiring OTP gives attackers a wide window to brute force or reuse codes
Timeline showing how a non-expiring OTP gives attackers a wide window to brute force or reuse codes


Every case, one table

Anti-patternWhat goes wrongWho got hit
OTP in the response bodyCode visible without accessing phone/emailMTN Group (three separate reports)
No rate limitingBrute-force every code in secondsDigits plugin (CVE-2025-4094), OpenCTI, Courier
Client-side validationAttacker changes "failed" to "success"Zomato (twice)
Session before verificationAuth token issued before OTP checkedShopify, HackerOne
Weak expiry and bindingCodes reusable, long-lived, or unboundGrab, Mars

None of these required zero-days or advanced tooling. A proxy tool, a curl command, and a few minutes.


What you should check right now

If your app uses OTP for anything — sign-up, login, password reset, sensitive actions — run through this list:

  1. Check your verification response. Send a wrong OTP. Read the full HTTP response. If the real code appears anywhere — body, headers, error message — fix it immediately.

  2. Test your rate limits. Send 20 wrong codes in a row. Does the endpoint lock you out? Slow you down? Invalidate the code? If you can send 100 attempts without consequence, an attacker can send 10,000.

  3. Verify server-side enforcement. Intercept a failed OTP response and change it to "success." If your app proceeds as if the code was valid, your check is client-side only.

  4. Confirm your session timing. Log in with a valid password, then check: do you have a session token before entering the OTP? If yes, the OTP is decoration.

  5. Scan it. Flowpatrol checks for these patterns automatically — missing rate limits, response leaks, client-side-only validation, weak expiry. Paste your URL and see what comes back.


Sources: All disclosures referenced in this article are publicly documented on HackerOne, WPScan, Exploit-DB, and GitHub Security Advisories.

Back to all posts

More in Case Study

One Line of Code Stole Your Emails: The First MCP Supply Chain Attack
Apr 7, 2026

One Line of Code Stole Your Emails: The First MCP Supply Chain Attack

Read more
The Replit Agent Deleted My Database. When I Told It to Stop, It Ignored Me.
Apr 7, 2026

The Replit Agent Deleted My Database. When I Told It to Stop, It Ignored Me.

Read more
Azure Sign-In Log Bypass: Four Bugs That Made Logins Invisible
Apr 6, 2026

Azure Sign-In Log Bypass: Four Bugs That Made Logins Invisible

Read more