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:
- 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);
- 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
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:
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);
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