Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/regex-le
A regex that backtracks catastrophically is a denial of service with a
code review that approved it. (\w+)+@ looks like an email check and
hangs a request thread on forty characters of input. regex-le finds
every pattern in a tree — literals and RegExp constructors, not the
division signs and URLs a grep would hand you — and tells you which ones
have that shape.
It never runs them. The verdict reads the pattern text, so scanning a repository is a cheap deterministic CI step with no engine, no timeout and nothing to sandbox.
It is the second frontend of
Regex-LE, the VS Code
extension — one product, two frontends, one repository, so the two can
never read a document differently. The corpus both build against lives
at
crate/fixtures/,
and CI fails on drift.
Sixty seconds
|
# the point of the whole thing:
||
./src/validate.js:1:15 /(\w+)+@/g [high] Nested unbounded quantifiers can cause exponential backtracking
./src/parse.ts:1:13 /(a|a)*/ [medium] Alternation with overlapping branches inside a quantifier
2 findings in 3 files
Exit codes: 0 nothing vulnerable, 1 at least one finding, 2 the
question was malformed. --severity sets where the line falls.
Install
| Route | Command | Worth knowing |
|---|---|---|
| cargo | cargo install regex-le |
Any platform, needs Rust 1.88+. |
| From source | git clone https://github.com/nolindnaidoo/regex-lecd regex-le/crate && cargo build --release |
The same build CI runs. |
No runtime, no network, nothing written.
It flags shapes; it cannot prove a pattern safe
This is a scanner, not an automaton analysis. It recognises the two shapes that cause almost all real ReDoS reports and says nothing about the rest. A pattern it does not flag may still backtrack badly on adversarial input.
That limit is worth stating plainly because the alternative reads the same and isn't: a tool that implied clearance would be worse than one that found less. Proving safety means building the automaton and checking it for ambiguity — a different tool, and one that costs seconds per pattern rather than microseconds.
| severity | shape | why |
|---|---|---|
high |
a quantified group whose body holds another unbounded quantifier — (a+)+, ([a-z]+)*, (\w*)+ |
exponential backtracking |
medium |
a quantified group whose body is an alternation with overlapping branches — (a|a)*, (a|ab)+ |
heavy backtracking |
low |
everything else, including patterns that do not compile | no obvious vulnerability, or a syntax error |
low is not offered as a --severity threshold: every pattern has a
verdict, so it would fail on any file holding a regex at all, and a
check that always fires is a check nobody reads. --all is how you see
them.
Half the extension, on purpose
The extension does two things. This ports one.
Ported — the lint. Find the patterns, judge their shape, over a tree instead of a buffer.
Not ported — the tester. Running your pattern against your text and showing the matches with capture-group positions is an interactive activity: you type, you look, you adjust. It belongs in an editor, and nobody runs it over a repository.
It is also the half that would be expensive to be honest about. Matching
with JavaScript semantics — backreferences, lookbehind, named groups,
lastIndex, the d flag's capture indices — needs a
JavaScript-compatible engine, and getting it nearly right would mean
this tool reporting different matches than the extension for the same
pattern. The lint half needs no such thing. Contract tests on both
surfaces assert that no flag and no tool schema offers text to match
against.
What it reads
Every text file in the tree. A regex literal is a regex literal wherever it appears, so there is no format filter and no language list.
A directory is walked the way ripgrep walks one: .gitignore honoured,
hidden files skipped, --no-ignore and --hidden to reach the rest. A
file named explicitly is always read.
Ported as-is, including the awkward parts
- A slash is only a regex when it can be one. After an identifier, a
number or a closing bracket it is division; after another slash it is
a comment. The list of keywords that may be followed by a regex —
return,case,yield,throwand the rest — is the extension's and is ported verbatim, because a second implementation guessing at it is how two frontends start disagreeing about what is even a pattern. - A pattern-and-flags pair is reported once, at its first occurrence. The output is a pattern list, not an occurrence list, which is why a file using the same validation regex ten times reports one finding.
- Constructors count, including ones split across lines:
new RegExp('…', '…')andRegExp("…"), with escaped quotes handled in both arguments. - An invalid pattern is a syntax error, not a vulnerability. It
comes back
lowwith the reasonPattern is invalid. - Columns are UTF-16 code units, 1-based, because that is what your editor shows you.
Options
--severity <level> fail at this verdict or worse: high or medium
(default medium)
--all report every pattern, not only the vulnerable ones
--stdin read one document from stdin
--hidden walk hidden files and directories too
--no-ignore walk files that .gitignore excludes
As an MCP server
Two tools, both returning { ok, data, diagnostics, meta }:
extract_patterns— content in, patterns and verdicts out. Touches no filesystem. The npm server ships the same tool with byte-identical output; one corpus runs against both.regex_le_lint— files or directories in, the same reports the CLI writes.
ok means the scan ran, never that it found something. A file with no
vulnerable pattern is a result, not an error.
The other four ways to run it
| Where | What you get | Install |
|---|---|---|
| VS Code | The lint and the tester, in your editor | Marketplace |
| Cursor, VSCodium, Windsurf | The same extension | Open VSX |
| Any MCP agent, via Node | extract_patterns over stdio |
npx regex-le-mcp · npm |
| Zed | The MCP server as a context server | add it by hand (no listing yet) |
All ten LE tools are on letools.dev.
Also by nolindnaidoo
Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. The five LE crates are the terminal half of the extensions they sit in — the same detection, held to the extension's own corpus, and an exit code instead of a results editor.
- pixelcoords — Freeze your screen, mark regions, get pixel-exact coordinates and crops pixelcoords.dev · crates.io · docs.rs
- pixelactions — Consume human-verified coordinates, perform the interaction, confirm it landed pixelactions.dev · crates.io · docs.rs
- paths-le — Find every path in a codebase and report whether it still points at anything crates.io
- secrets-le — Find hardcoded credentials, and never print one crates.io
- urls-le — Extract every URL from a codebase, with its protocol and exact position crates.io
- scrape-le — Check whether a page is scrapeable before the scraper is written crates.io
Contact Developer — nolindnaidoo.com · GitHub · LinkedIn
License
MIT — see LICENSE.