ReDoS: how one regular expression can stall a server
Some regular expressions take exponential time on input that almost matches. In Node.js, where one event loop serves every request, one such match stops the whole server.
Example
const BANK_ROUTING = /([0-9]+)+\#/;
app.post("/profile", (req, res) => {
if (!BANK_ROUTING.test(req.body.bankRouting)) return res.sendStatus(400);
save(req.body);
});const BANK_ROUTING = /^[0-9]{1,17}#$/;
app.post("/profile", (req, res) => {
const value = String(req.body.bankRouting).slice(0, 32);
if (!BANK_ROUTING.test(value)) return res.sendStatus(400);
save(req.body);
});How it happens
A repeated group whose contents can themselves repeat — (a+)+, ([0-9]+)+, (\w+\s?)+ — gives the engine many ways to split the same input. When the overall match fails, it tries all of them. Thirty digits followed by a character that breaks the match can take seconds; forty can take minutes. Compiling a pattern from user input (new RegExp(req.query.q)) hands the attacker the pattern itself.
How to fix it
Make each repetition unambiguous.
- Remove the nested quantifier: ([0-9]+)+ becomes [0-9]+
- Anchor the pattern and bound repetition counts ({1,17})
- Cap input length before matching
- Escape user input before building a pattern, or compare strings instead
How RepoVerse finds it
- HighRequest input is matched against a backtracking-prone regular expression
Nested quantifiers backtrack exponentially. A few dozen crafted characters pin the CPU, and in Node's single event loop that stalls every request on the server.
data flow · CWE-1333
- ModerateRequest input is compiled into a regular expression
The caller supplies the pattern itself, including one that backtracks forever.
data flow · CWE-1333
- LowRegular expression that can be made to hang
Nested quantifiers like `(a+)+` backtrack exponentially, so a crafted input of a few dozen characters pins a CPU core.
pattern · CWE-1333
Questions
- Is every nested quantifier dangerous?
- No. If each repetition must start with a fixed character the rest of the group cannot match — (\.\d+)* in a version number — there is only one way to split the input and matching stays linear. RepoVerse tells these apart instead of flagging every nested group.
- Why is ReDoS worse in Node.js?
- Regular expressions run on the main thread, so a slow match blocks every other request on the server until it finishes.
Related
- Code injection: request input passed to evalWhy eval, new Function and string timers on request data mean remote code execution (CWE-95), a real example from OWASP NodeGoat, and the fix.
- GitHub security scanner for code, secrets and dependenciesScan a GitHub repository for injection flaws, leaked secrets and vulnerable dependencies. Data-flow traces from request to sink, lockfile-aware, with fixes.
Updated 2026-10-11