Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Regular Expression Denial of Service (ReDoS)

Regular Expression Denial of Service (ReDoS)

Here is an example of JavaScript code that is vulnerable to regular expression denial of service:

🥺 Vulnerable Code

// Vulnerable: nested quantifiers cause catastrophic backtracking
const EMAIL = /^([a-zA-Z0-9_.-]+)+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;

app.post("/api/subscribe", (req, res) => {
  if (!EMAIL.test(req.body.email)) {
    return res.status(400).json({ error: "Invalid email" });
  }

  return subscribe(req.body.email).then(() => res.json({ subscribed: true }));
});

([a-zA-Z0-9_.-]+)+ gives the engine an exponential number of ways to split the same input. A string of forty a characters with a trailing ! and no @ forces the backtracking engine through billions of attempts. Node runs your handlers on one thread, so a single request of a few dozen bytes freezes the entire process for every other user.

😎 Secure Code

Here is a version of the same code that is secured against regular expression denial of service:

// Linear-time validation: bounded lengths, no nested quantifiers
const EMAIL = /^[a-zA-Z0-9_.+-]{1,64}@[a-zA-Z0-9.-]{1,185}$/;

app.post("/api/subscribe", express.json({ limit: "8kb" }), (req, res) => {
  const email = String(req.body.email || "");

  if (email.length > 254 || !email.includes(".") || !EMAIL.test(email)) {
    return res.status(400).json({ error: "Invalid email" });
  }

  return subscribe(email).then(() => res.json({ subscribed: true }));
});

Every quantifier is bounded and none of them nest, so matching is linear in the length of the input, and the length is capped before the regex ever runs. When a format genuinely needs a complex pattern, use a real parser or a linear-time engine such as the re2 bindings, and run eslint-plugin-security or a ReDoS checker in CI to catch (x+)+ and (a|a)* shapes before they ship.