Regex is the difference between staring at a 40MB JS bundle and pulling every endpoint out of it in one command. This page has five parts: a cheat sheet for every character that does something, a live builder that explains your pattern token by token, fourteen graded exercises built on real hunting output, a flavour comparison for the engines you actually run into, and a playground for catastrophic backtracking. Everything runs locally in your browser, and every match is evaluated inside a killable worker so a runaway pattern cannot freeze the page.
Engine: JavaScript / ECMAScript RegExp
There is no such thing as "regex" in general. The builder and the exercise checker on this page
run the JavaScript engine, so lookbehind works, atomic groups and possessive quantifiers do not, and
(?i) is a syntax error. That is not the same regex your other tools speak: nuclei and ffuf are Go RE2 and
reject lookaround entirely, Burp is Java, grep -P and every WAF are PCRE2, ripgrep is Rust regex unless
you pass -P. Every row of the cheat sheet carries a flavour column, every exercise says whether its answer
is portable, and the Flavours tab has the full matrix.
Every regex is a tiny program made of four kinds of thing: literals (match themselves), classes (match one character out of a set), quantifiers (say how many times the thing before them repeats), and assertions (match a position, not a character, and consume nothing). Once you can tell which of those four you are looking at, any pattern becomes readable.
Tested with the JavaScript engine. (?i),
(?>...), a++, \K, \A and \z will not compile
here - use the flag boxes instead, and check the Flavours tab before pasting a pattern into nuclei, Burp or grep.
Each exercise gives you a chunk of realistic output and asks for a pattern that matches exactly the
right substrings - no more, no less. Extra matches fail the check, because in hunting a pattern that also grabs
nottarget.com is worse than no pattern at all. Progress is stored in this browser only.
Grading engine: JavaScript / ECMAScript. Each exercise is labelled either portable - the answer works on every common engine including Go RE2, so you can paste it into a nuclei template - or flavour specific, where the answer needs lookaround or a backreference and will be rejected by RE2 based tools. The per-exercise note says exactly which.
"Regex" is a family, not a language. The same pattern can be valid, invalid, or valid-but-different depending on the engine underneath the tool you typed it into. Before you blame your pattern, check which engine you are talking to - most "why does this work in my tester but not in nuclei" moments are this table.
The two questions that decide almost everything: does it backtrack (if yes you get lookaround and backreferences, and you also get ReDoS), and is it POSIX (if yes you lose lazy quantifiers and non-capturing groups).
Anything in the "no" column is not a limitation you can work around with a cleverer pattern - the engine cannot express it at all. You move the logic out of the regex instead.
| Tool | Engine | What trips people up |
|---|---|---|
| This page, browsers, Node | ECMAScript | No (?i), no atomic groups, no possessive quantifiers, no \A/\z. Lookbehind does work. |
| nuclei templates | Go RE2 | No lookaround, no backreferences. Named groups are (?P<name>...). Rejects the pattern outright rather than misbehaving. |
| ffuf filters, Go tooling | Go RE2 | Same as nuclei. |
| Burp Suite (match/replace, grep, Intruder) | Java | Full backtracking. Possessive quantifiers and atomic groups are available here, so ReDoS fixes are easy - and so is hanging Burp on a big response. |
grep | POSIX BRE | + ? { } | ( ) are literals unless backslashed. Use -E for ERE. |
grep -P | PCRE2 | GNU only - missing on macOS and busybox. Gives you \K, which makes -o extraction trivial. |
ripgrep | Rust regex | Linear time, so no lookaround or backreferences. -P / --pcre2 switches engines and gets them back. |
sed, awk | POSIX BRE / ERE | No lazy quantifiers at all. .*? is literally "any characters, then an optional question mark". |
jq test/match | Oniguruma | PCRE-like, has lookaround. Flags go in a second argument: test("x";"i"). |
| Python | re / regex | re lookbehind must be fixed width. \w and \d are unicode aware by default, which quietly widens your secret patterns. |
Semgrep pattern-regex | PCRE2 | Full featured, but remember it runs over source, not over a rendered page. |
Elasticsearch regexp, Lucene | Lucene subset | Anchored to the whole term by default and no lookaround. Wrap in .* for substring behaviour. |
| Splunk, ModSecurity, CloudFlare, Snort, Suricata, Wireshark | PCRE2 | Backtracking in the request path - which is why a rule itself can be the DoS. |
| Postgres / MySQL 8 / MariaDB / Oracle | ARE / ICU / PCRE / POSIX | Four different answers in one stack. Always test against the real database. |
| VS Code, IntelliJ | ECMAScript / Java | Your editor and your scanner disagree, which is how a working pattern "breaks" on the way to CI. |
Going from JS or PCRE to Go RE2 (nuclei, ffuf) or Rust regex (ripgrep) means giving up lookaround and backreferences. You do not rewrite them - you move that part of the job outside the regex.
| What you had | What to do instead |
|---|---|
(?<=https:\/\/)[\w.-]+ | Capture it: https://([\w.-]+) and read group 1. Nuclei calls this an extractor with group: 1. |
id=\K\d+ (PCRE) | Same trick - id=(\d+) plus group 1. |
^(?=.*[A-Z])(?=.*\d).{12,}$ | Split into separate checks and AND them in the tool: nuclei condition: and with one regex per requirement. |
^(?!.*admin) | Invert at the tool level: a negative matcher, grep -v, or negative: true in nuclei. |
\b(\w+)\s+\1\b | Cannot be done on RE2 at all. Match the candidates and compare them in code. |
(?>\w+)@ or \w++@ | In JS: (?=(\w+))\1@. On RE2 you do not need it - it cannot backtrack anyway. |
(?i)AKIA... in JS | Move it to a flag: /AKIA.../i. JS has no inline flags. |
[[:alpha:]] in JS | Spell the class out: [A-Za-z], or \p{L} with the u flag. |
\A / \z in JS | ^ and $ without the m flag mean the same thing. |
| A pattern that is fine in Python | Remember \w and \d are unicode there. Add re.ASCII if you meant ASCII, or your "hex token" pattern starts matching Arabic-Indic digits. |
A regex engine that backtracks tries every way of splitting the input between its quantifiers. Nest one quantifier inside another over the same characters and the number of ways grows exponentially with input length, so a 30 character string can take longer than the heat death of your shift. If a server compiles user input into a regex, or runs a vulnerable pattern over user input, that is a denial of service bug - and it is one of the few places where a single crafted string takes an endpoint offline.
| Pattern | Why it explodes | Killer input |
|---|---|---|
(a+)+$ | Quantifier inside a quantifier over the same character | "a".repeat(30) + "!" |
(\w+\s?)*$ | \w and \s? overlap, so splits multiply | 30 spaces then ! |
^(\d+)*x$ | Same as above with digits | 25 digits, no x |
(.*,)*z$ | .* repeated - the worst of all worlds | 25 commas |
^(a|a)+$ | Two alternatives that match the same thing | "a".repeat(28) + "b" |
Notice the pattern: ambiguity plus repetition plus a forced failure at the end. The trailing
$ or literal is what makes the engine explore every split before giving up.
The match runs in a Web Worker. If it has not finished in two seconds the worker is terminated, which is exactly what your target's request thread cannot do.
| Fix | Before | After |
|---|---|---|
| Remove the inner quantifier | (a+)+$ | a+$ |
| Bound the repetition | (\w+\s?)* | (?:\w{1,32}\s?){0,64} |
| Make the parts unambiguous | (\d+|\w+)+ | \w+ |
| Anchor and use a negated class | ".*" | "[^"]*" |
| Emulate an atomic group (JS has none) | (?:a+)+b | (?=(a+))\1b |
| Use a linear engine | PCRE backtracking | RE2, Go regexp, Rust regex, ripgrep |
Reporting one: show the pattern, the input, and a measured time curve (20, 25, 30 characters). A graph that doubles per character is what turns "slow endpoint" into an accepted DoS finding. Check the program's rules first - plenty of programs exclude DoS entirely.