Skip to content

[Security]: OTP verify endpoint has no rate limiting — vulnerable to brute-force OTP bypass #1007

Description

@prince-pokharna

Summary

In api/routes/otpRoutes.js, the /send-otp endpoint is correctly rate-limited to 5 requests per IP per 15 minutes. However, the /verify-otp endpoint has no rate limiting at all. A 6-digit OTP has only 1,000,000 possible values. An attacker who intercepts or guesses a target's phone number can send automated requests to /api/verify-otp cycling through OTP values until they find the correct one — bypassing the entire phone verification flow and gaining unauthorized access to place orders under someone else's number.

Problem

From api/routes/otpRoutes.js:

js
router.post('/send-otp', otpRateLimiter, sendOtp); // ← rate limited ✓
router.post('/verify-otp', verifyOtp); // ← NO rate limiting ✗
An attacker can send up to 1,000,000 POST requests to /api/verify-otp with different OTP values in rapid succession.
The OTP is valid for 5 minutes (300 seconds). At 100 requests/second, all 1M combinations can be tried in ~2.8 hours — well within the window.
There is no failed-attempt lockout — after 5 wrong guesses, the endpoint continues accepting attempts without consequence.
There is no CAPTCHA or secondary verification layer.
Proposed Solution

I will apply rate limiting to the verify endpoint and add a failed-attempt lockout:

  1. Rate limit on /verify-otp:

js
const verifyRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // max 10 verification attempts per IP per window
message: { success: false, message: 'Too many verification attempts. Please request a new OTP.' },
});

router.post('/verify-otp', verifyRateLimiter, verifyOtp);

  1. Failed-attempt counter on the OTP document — Add a failedAttempts field to the Otp model. After 5 failed attempts, mark the OTP as used: true (invalidating it) and require the user to request a new one.

js
// In verifyOtp controller:
if (!record) {
await Otp.updateOne({ phone, used: false }, { $inc: { failedAttempts: 1 } });
const otp = await Otp.findOne({ phone, used: false });
if (otp?.failedAttempts >= 5) {
await Otp.updateMany({ phone, used: false }, { used: true });
return res.status(429).json({ success: false, message: 'Too many failed attempts. Please request a new OTP.' });
}
return res.status(400).json({ success: false, message: 'Invalid or expired OTP' });
}

This is a targeted security fix that can be implemented in under 30 lines of code with significant impact.

Could you assign this issue to me?

Labels: security, bug, GSSoC 2026

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions