
One nested-quantifier group inside a single JavaScript regex can pin a Node event loop at 100% CPU for ten seconds on a thirty-character input. That regex is almost always hiding behind a username validator, an email parser, a webhook signature check, or a search box. It is the same pattern that passed review last quarter because the test set only had ten well-formed strings. The ReDoS (Regular Expression Denial of Service) Scanner finds those patterns before your Friday-afternoon incident does. Paste a regex, set a max evil-input length, click scan, and you get a risk score, a risk level, a static-heuristic findings list, a worst simulated near-miss execution time, and a safer rewrite suggestion.
What ReDoS actually is, and the five shapes it takes

ReDoS is the denial-of-service you get when a regex engine has to backtrack exponentially on a near-miss input. A classic trigger is a nested quantifier like (a+)+$. On aaaaaaaab the engine has to try every combination of how the inner + and the outer + carve up the as before it admits there is no b at the end. The input is small. The work is not. All three flavors are detectable from the pattern alone — you do not have to wait for a production slowdown to confirm them:
– Exponential backtracking on patterns like (a+)+$ or (\w+\s?)*$.
– Polynomial backtracking on patterns with overlapping alternations, like (a|a)*$.
– Catastrophic greedy wildcards like .*.* on long unanchored input.
The scanner’s static-heuristics engine looks for five distinct shapes that catch all three families. Each shape contributes a fixed slice to the risk score.
– Nested quantified group — (a+)+$, (\w+\s?)*$, (foo|bar|baz)+ inside another quantifier. +40 to the score.
– Multiple greedy wildcards — two .* or two .+ in the same pattern. +20.
– Alternation inside a repeated group — (foo|bar)+ or (a|b|c)*. +20.
– Repeated quantified atom — (a+)+ (which is also the first one), (a+)*+, or (a+)+*. +30.
– Unanchored pattern — missing ^ at the start or $ at the end. +5.
The thresholds are calibrated to the JavaScript engine, which is the most common runtime for HTTP-driven ReDoS exposure. A pattern that hits three of the five will land in the critical band before the benchmark even runs.
How the benchmark runs, and how the scores map to risk levels

Static heuristics are fast but conservative. The scanner also simulates worst-case execution.
1. Build an evil seed character based on the pattern’s character class — a if a appears, 1 if d appears, and so on.
2. Generate near-miss inputs at lengths [8, 12, 16, 20, 24, 28, 32, maxLength], each ending in ! so they fail to match but only after the engine has done the most work.
3. Run each input through the regex scaledRuns times (capped at 25 to keep the scanner responsive), timing the wall-clock elapsed.
4. Track the worst case and flag benchmarkTimedOut if any run exceeds 40ms.
5. Skip the benchmark entirely if the static risk score is already 55 or higher — the pattern is already known-dangerous, and probing it directly could freeze the scanner itself.
The output is the worst-case elapsed time in milliseconds, a sample evil input that produced it, a boolean for benchmarkTimedOut, and a risk level computed from the combined static-plus-runtime score. Four bands, four colors, one decision.
– safe — score below 25. No nested quantifiers, no multi-greedy, no alternation in repeats, and the pattern is anchored.
– watch — 25 to 49. One anti-pattern, or several weak ones. Worth a second look but usually fine.
– high — 50 to 74. Multiple anti-patterns, or one strong one plus anchoring problems. Refactor before shipping.
– critical — 75 or higher. Nested quantified group plus repeated quantified atom. Block the PR, do not deploy.
The scanner maps (score) -> riskLevel via a single helper. If you need to log the score for compliance, every ScanFinding record carries both riskScore and riskLevel so you do not have to recompute.
Four patterns you will see in a code review, and what the scanner reports

Two fail outright. Two look scary but pass.
Fail #1 — (a+)+$. Textbook case. Inner a+ consumes one or more as. Outer + consumes one or more repetitions of that. On aaaaaaab, the engine has to try every possible partition of the as across the outer repetition before admitting the trailing b does not match. The scanner flags it with the nested-quantified-group heuristic and the repeated-quantified-atom heuristic. Static score 70 before the benchmark even runs.
Fail #2 — (\w+\s?)*$. Worse, because w matches [A-Za-z0-9_] and s? matches an optional space. The same near-miss aaaaaaaaaaaaaaaaaaaaaab forces the engine to try every combination of where the w+ and s? boundaries fall. Static score hits critical on the first heuristic alone.
Pass #1 — (foo|bar)+$. Looks like it should fail (alternation inside a group), but the repetition is linear, not exponential. Static score 20 — watch, not high. Worth a note in the PR but no need to refactor.
Pass #2 — ^[a-z]+$. Linear, no nesting, single character class. Static score 0 — safe. The scanner handles both correctly because the score is additive and the heuristics are independent. A pattern with only one weak anti-pattern lands in watch, not critical.
The safer rewrites the scanner suggests
The scanner’s rewrite suggestions follow three recipes:
– Replace nested quantifiers with possessive quantifiers where the engine supports them (JavaScript does not yet, so this is a hint, not a drop-in).
– Replace (\w+\s?)* with [A-Za-z0-9_ ]*$ — a single character class absorbs the alternation in linear time.
– Add explicit anchors — wrap the pattern in ^...$ and the engine can fail fast on a non-matching prefix instead of trying every partition.
For the (a+)+$ example, the recommended rewrite is ^a+$. For the (\w+\s?)*$ example, the rewrite is ^\w+(?:\s\w+)*$. Both are linear and both pass the same test suite the original was meant to catch.
How to wire the scanner into CI
Three options, in order of effort.
– On-demand — paste a regex into the tool, read the verdict, ship or fix. Good for one-off reviews.
– Pre-commit hook — call the scanner’s underlying heuristics (or the same scoring function) on every pattern in a validators.ts file. Block on high or critical.
– HTTP integration test — instrument a small Node script that loops every validator pattern through new RegExp(...) against a 30-character near-miss and times it. If any run exceeds 50ms, fail the build.
The ReDoS Scanner is the first option. The heuristics and benchmark logic are the same primitives the second and third options run.
What to do after you find a critical pattern
Three steps, in order.
– Refactor the regex using one of the rewrite recipes above.
– Add an input-length cap in the validator layer. Even an exponential regex cannot freeze the loop if the input is bounded at 200 characters.
– Add a metric for regex.test(input) execution time in your APM. A p99 over 50ms on a regex.test call is the smoking gun.
The third step matters because the static heuristics cannot catch every pattern. If you only fix the patterns the scanner flags and ship without runtime observability, the next novel ReDoS will still take down your endpoint.
Other security tooling worth pairing with this
The ReDoS scanner is one piece of a defense-in-depth stack. Pair it with these.
– The Security Headers Checker — surfaces missing HSTS, CSP, and X-Frame-Options headers that compound a regex-induced outage by blocking the mitigation options browsers would normally provide. – The Entropy Calculator — confirms that any token you generate for use in a regex-driven validator is high-entropy enough to resist near-miss brute-forcing. – The JWT Decoder & Security Auditor — JWTs are one of the most common regex-validation surfaces, and weak patterns in the aud or iss check are a frequent ReDoS vector.
The three together cover the request-path attack surface that a single ReDoS scanner cannot.
Putting it together
A ReDoS bug is the rare security issue you can find with a regex and a calculator, before an attacker ever sees your endpoint. The static heuristics cover the textbook patterns. The benchmark covers the novel ones. The risk level gives you a one-glance decision. The safer rewrite is right there in the output, ready to paste.
Run every new regex through the ReDoS Scanner before you merge. If the verdict is watch, refactor it anyway. If the verdict is high or critical, do not ship it. The cost of running the scan is five seconds per pattern. The cost of a ten-second event-loop freeze in production is your whole weekend.
For more tools in the security cluster, browse the elysiatools.com catalog.