Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Missing Rate Limiting on APIs

Missing Rate Limiting on APIs

Here is an example of Node.js code that is vulnerable to missing rate limiting:

🥺 Vulnerable Code

// Vulnerable: an expensive, abusable endpoint with no limits at all
app.post("/api/otp/send", async (req, res) => {
  const { phone } = req.body;

  const code = generateOtp();
  await store.saveOtp(phone, code);
  await sms.send(phone, `Your verification code is ${code}`);

  res.json({ sent: true });
});

Every call costs real money and there is nothing stopping a script from making a hundred thousand of them. That is SMS pumping, and the bill arrives before anyone notices. The same gap on the verify endpoint lets an attacker brute force a six digit code in minutes, and on login it turns a leaked credential dump into a working credential stuffing run.

😎 Secure Code

Here is a version of the same code that is secured against missing rate limiting:

const rateLimit = require("express-rate-limit");
const { RedisStore } = require("rate-limit-redis");

const otpLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 5,
  standardHeaders: "draft-7",
  legacyHeaders: false,
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  keyGenerator: (req) => `${req.body.phone}|${req.ip}`,
});

app.post("/api/otp/send", otpLimiter, async (req, res) => {
  if (!isValidPhone(req.body.phone)) {
    return res.status(400).json({ error: "Invalid phone number" });
  }

  if ((await store.otpCountToday(req.body.phone)) >= 10) {
    return res.status(429).json({ error: "Daily limit reached" });
  }

  const code = generateOtp();
  await store.saveOtp(req.body.phone, code);
  await sms.send(req.body.phone, `Your verification code is ${code}`);

  res.json({ sent: true });
});

The counter lives in Redis, so the limit holds across every instance behind the load balancer instead of resetting per process. Keying on the phone number as well as the IP stops an attacker rotating through a proxy pool, the daily cap bounds the worst case, and a 429 with standard headers tells honest clients when to retry. Pair it with exponential backoff and lockout on the verify side, spend alerts at the SMS provider, and an edge rate limit as a second layer.