Safeguard
Security Guides

ReDoS: Regular Expression Denial of Service in JavaScript

A single bad regex can freeze your entire Node.js event loop on one malicious request. Here is how catastrophic backtracking works, how to spot vulnerable patterns, and how to fix them without rewriting everything.

Priya Mehta
Security Researcher
6 min read

ReDoS is the vulnerability that hides in one line of code you'd never suspect: a regular expression. Because Node.js runs your JavaScript on a single thread, a regex that takes exponential time to evaluate doesn't just slow down one request — it blocks the entire event loop, freezing every other request the process is handling. An attacker who finds one vulnerable pattern can take your service offline with a single, tiny HTTP request. This is not theoretical: a catastrophic-backtracking regex in a web application firewall rule caused a major global outage in July 2019. This guide explains the mechanism, shows you how to recognize the danger, and gives you safe fixes.

Why one regex can freeze everything

Node's event loop is single-threaded for JavaScript execution. When you call pattern.test(input), that runs synchronously to completion — nothing else executes until it returns. Most regex evaluations are fast, so this is fine. But certain patterns, on certain inputs, take time that grows exponentially with input length. Feed such a regex a 40-character string and it might perform trillions of operations, pinning a CPU core for seconds or minutes. During that time your server accepts no connections, serves no other users, and answers no health checks. It looks, from the outside, exactly like a crash.

Catastrophic backtracking, concretely

JavaScript's regex engine (like most) uses backtracking: when a match fails, it goes back and tries other ways to match. The danger is a pattern where the number of ways to try explodes. The classic shape is a quantifier applied to a group that itself contains a quantifier, matching overlapping input — "nested quantifiers."

// Vulnerable: (a+)+ — nested quantifier over overlapping matches
const evil = /^(a+)+$/;

evil.test("aaaaaaaaaaaaaaaaaaaaaaaa!"); // the trailing ! forces failure
// The engine tries every way to split the a's between the inner and
// outer +, an exponential number of combinations, before giving up.

Add one more a to the input and the time roughly doubles. That's the exponential blowup. The trailing character that can't match is what forces the engine to exhaust every combination before it can conclude "no match."

Real-world vulnerable patterns are subtler than (a+)+. They show up in email validators, URL parsers, and whitespace-trimming expressions — anywhere alternation or nested quantifiers overlap.

// A realistic vulnerable validator
const badEmail = /^([a-zA-Z0-9]+)*@example\.com$/;
// Vulnerable trimming pattern
const badTrim = /(\s+)+$/;

How to spot a vulnerable pattern

Watch for these red flags in any regex that touches untrusted input:

  • Nested quantifiers: (x+)+, (x*)*, (x+)* — a quantified group inside another quantifier.
  • Overlapping alternation under a quantifier: (a|a)*, (\w|\d)* where the branches can match the same characters.
  • Adjacent, ambiguous quantifiers around similar character classes: \s*\s*, .*.*.

The unifying property is ambiguity: if there are many different ways the engine could match the same substring, backtracking has many paths to explore.

Four ways to fix it

1. Rewrite to remove ambiguity

Most ReDoS patterns can be flattened. ^(a+)+$ is equivalent to ^a+$, which is linear-time. Collapse nested quantifiers wherever the extra grouping adds no real capability.

const safe = /^a+$/;         // linear, no backtracking blowup
const safeTrim = /\s+$/;     // instead of (\s+)+$

2. Bound the input before matching

Even a slightly risky regex is defanged if the input can't be long enough to blow up. Cap length at the boundary:

if (input.length > 256) return res.status(400).send("too long");
if (pattern.test(input)) { /* ... */ }

Exponential growth is only dangerous when the attacker controls input length; a hard cap turns "minutes" into "microseconds."

3. Prefer non-backtracking approaches

For structured formats — emails, URLs, dates — use a purpose-built parser or a validation library instead of a hand-rolled regex. Parsers don't backtrack catastrophically. For simple checks, plain string methods (startsWith, includes, split) are immune to ReDoS entirely.

4. Run untrusted matching off the main thread or with a timeout

When you must run a potentially expensive regex on untrusted input, isolate it. Node's worker_threads lets you run the match in a worker you can terminate if it exceeds a time budget, so a pathological input kills a worker instead of your whole server. Modern regex libraries with linear-time guarantees (RE2-style engines exposed to Node) are another strong option for user-supplied patterns.

A ReDoS defense checklist

ControlEffect
Audit regexes for nested/overlapping quantifiersFind the vulnerable patterns
Rewrite to remove ambiguityEliminate backtracking blowup
Cap input length at the boundaryBound worst-case time
Use parsers/string methods for structured inputAvoid regex risk entirely
Linear-time engine or worker + timeout for user patternsContain the blast radius
Never build regexes from untrusted stringsPrevent attacker-defined patterns

The last row deserves emphasis: constructing a new RegExp(userInput) lets an attacker supply the vulnerable pattern and the input — the worst case.

How Safeguard helps

Vulnerable regexes hide in both your code and your dependencies, and they're easy to miss in review because they look ordinary. Safeguard's software composition analysis flags dependencies with known ReDoS CVEs — a recurring class of advisory in popular parsing and validation packages — and uses reachability analysis to tell you whether the vulnerable pattern is actually exercised by your code. Griffin AI inspects regular expressions for catastrophic-backtracking risk and suggests linear-time rewrites, and dynamic testing can probe endpoints with ReDoS payloads to confirm an input path can stall the event loop. Run the checks in CI via the Safeguard CLI so a newly introduced dangerous pattern is caught before it ships. See the comparison hub for how this fits alongside your other tooling.

Get started

ReDoS is a quiet, high-impact bug: one line, one request, a frozen service. Audit your patterns for nested quantifiers, cap your input lengths, and let automated analysis cover the regexes you didn't write. Create a free account at app.safeguard.sh/register and read the analysis docs at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.