Regular expressions and untrusted input: ReDoS, regex engine bugs and CVE-2026-97229
Why a regex built from user input is dangerous: slow matching (ReDoS) is one risk, memory-corrupting compiler bugs like GitLab's CVE-2026-97229 are another, and what to do about both.
· 7 min read
Written by AI from the sources linked below. How we use AI
On this page
In short
A regular expression is a small program that your regex engine compiles and runs. If an attacker controls the pattern, they control that program. Two different things can go wrong. The familiar one is ReDoS: the engine backtracks for an absurdly long time and your service hangs. The rarer and worse one, shown this month by CVE-2026-97229 in GitLab, is a bug in the engine's compiler that corrupts memory and can lead to running code. Timeouts help against the first and, according to Ruby's maintainers, do not protect against the second. The safe rule is the same for both: never build a regex out of untrusted text.
What a regex engine actually does
When you write /a+b/, your language does not scan the text with a fixed routine. It first compiles the pattern into an internal program, a list of instructions such as "match this character", "try this branch, and if it fails go back and try that one", or "repeat this group at most n times". Then it runs that program against your input.
That is why two separate risks exist. The compile step handles the pattern, and the run step handles the pattern and the text together. An attacker who controls only the text can attack the run step. An attacker who controls the pattern can attack both.
Risk one: catastrophic backtracking (ReDoS)
Most popular engines, including those in Perl, PCRE, Python, Ruby and Java, are backtracking engines. Russ Cox's classic article on regular expression matching explains the idea: a backtracking engine tries one choice at a time and returns to try the alternative when a path fails. That is simple and fast for ordinary patterns, but its worst case is exponential.
His example is the pattern a? repeated n times followed by a repeated n times, matched against a string of n letters a. The match only succeeds if every optional a? is skipped, and a backtracking engine tries both matching and skipping each one, giving 2 to the power n possibilities. In his measurements, Perl needed over sixty seconds to match a 29-character string, while a different approach, Thompson's NFA simulation, took about 20 microseconds. At n = 100, he says, the Thompson approach finishes in under 200 microseconds while Perl would need over 10 to the power 15 years. Engines built on Thompson's idea track all reachable states at once and read the input in one pass, so their cost is bounded by pattern length times text length. Awk, Tcl and GNU grep use that family of techniques, he notes.
OWASP describes the same problem as Regular expression Denial of Service. It calls a pattern an "Evil Regex" if it can get stuck on crafted input, and gives the typical shape: a group with repetition, where the repeated group itself contains either a repetition or an overlapping alternation. Its examples include (a+)+$, ([a-zA-Z]+)*$, (a|aa)+$ and (a|a?)+$. For ^(a+)+$, OWASP says, the number of paths doubles with each extra a: 16 paths for aaaaX, but 65,536 for sixteen a characters followed by X.
Two ways ReDoS reaches you
- Your own pattern is evil, the input is hostile. A validation regex for emails or class names, written years ago, is fed a long crafted string by an attacker. OWASP lists an email validation pattern from RegExLib and a Java classname pattern from its own validation repository as real examples.
- The attacker supplies the pattern. OWASP calls this regex injection: if user input becomes part of a regex, an attacker can supply an Evil Regex as, say, a username and hang the application.
Risk two: bugs in the regex compiler
ReDoS makes a program slow. A compiler bug can make it unsafe. This month's example is documented in detail by several sources, and it is a good case study because the cause is not in GitLab's code but in the engine underneath.
GitLab's 12 October patch release fixed CVE-2026-97229, which it rates CVSS 10.0 and describes as an integer overflow in the CI/CD regular expression compiler. Under certain conditions an unauthenticated user could run arbitrary code on the GitLab server using a specially crafted regular expression in CI/CD configuration.
Ruby's maintainers explained the underlying bugs on 23 October. They are in Onigmo, Ruby's regex engine, and fixed in Ruby 3.3.13, 3.4.12 and 4.0.9. They listed four, all reported by GitLab: a double free when pattern parsing fails after an alternation (CVE-2026-89078), an integer overflow in the size of a compiled pattern (CVE-2026-93577 and CVE-2026-97229) and an overflow of internal IDs in a pattern with many repeats (CVE-2026-110264). Ruby says specially crafted patterns can corrupt memory during compilation.
Rapid7's analysis shows the mechanics for CVE-2026-97229. GitLab's scanner checks repeat bounds such as {n} to keep them small, but it misses a quantifier split by an inline comment, so a bound above 1,000 gets through. The expanded program size then overflows a signed 32-bit check, and a miscalculated jump sends execution into bytes the attacker controls. Rapid7 reproduced command execution as the git user on one specific build, and notes that its demonstration needs a malicious CI configuration and a specially named path pushed to a public project, which it says conflicts with GitLab's "no privileges required" rating. We covered the incident at Alpine Linux, where the fix also needed a patched Ruby, in our report.
Why timeouts and length limits are not enough
The most common defences against slow regexes are a timeout and a cap on pattern length. Ruby's note says neither is reliable against the compiler bugs: Regexp.timeout does not protect against them, and limiting pattern length is unreliable because some of the bugs are triggered by short patterns. The reason is visible in the mechanism above. A timeout interrupts a long-running match, but a memory-corrupting bug can fire during compilation, before matching begins, in a handful of instructions.
That does not make timeouts useless. For the first risk, a hung match, a timeout is a reasonable last line of defence, and some languages let you set one per pattern or per process. It simply is not a fix for untrusted patterns.
A practical checklist
If users supply text that you match against fixed patterns:
- Prefer engines with linear-time guarantees for anything exposed to untrusted input. Cox's article describes the technique.
- Review your own patterns for the Evil Regex shape: a repeated group that contains another repetition or overlapping alternatives. Replace
(a+)+style nesting with a flat form where you can. - Cap the input length before matching. A limit on input is far more dependable than a limit on patterns.
- Set a match timeout where your language supports one, and log when it fires.
If users supply anything that becomes part of a pattern:
- Do not compile it. Escape it so it is treated as literal text (in Ruby,
Regexp.escape) or use plain substring search. - If you must let users write patterns, such as a search-and-replace feature or a CI rule, run the matching in a separate, resource-limited process and use an engine designed for untrusted patterns.
- Treat the engine as part of your attack surface. Subscribe to your runtime's security announcements, and apply runtime updates as promptly as application updates.
If you run software that embeds a regex engine:
- Know which runtime it bundles. Alpine's incident shows that updating GitLab without the matching Ruby left the server exposed, and GitLab's own packages carry patches that stock Ruby builds do not.
- Check the fixed versions: for the Ruby bugs above, 3.3.13, 3.4.12 or 4.0.9 and later.
Testing your patterns
A quick check for ReDoS is to feed a pattern a long run of the characters it accepts followed by one it rejects, as OWASP does with a run of a and a final !. Increase the length step by step. If the time grows roughly in step with the length, that is a good sign. If it explodes after a few steps, the pattern is vulnerable. Do this in a test environment, not in production, and set a hard timeout on your test run.
Common misunderstandings
- "Regex is just text matching, so it cannot be dangerous." It is a compiled program with its own memory behaviour, and engines are written in languages where memory bugs happen.
- "ReDoS only matters for public websites." Any service that matches text from outside, including CI systems, log pipelines and email filters, can be hung by it.
- "Our regex is short, so it is safe." Ruby's note says some of the compiler bugs trigger with short patterns, and OWASP's evil examples are only a few characters long.
Further reading
- OWASP, Regular expression Denial of Service (ReDoS)
- Russ Cox, Regular Expression Matching Can Be Simple And Fast
- Ruby, Regexp fixes for GitLab CVEs
- Rapid7, technical analysis of CVE-2026-97229
- GitLab, patch release 19.4.2, 19.3.5, 19.2.8
Sources
Written by
The official account of the people who run audiotale.net: announcements, conversation starters and the newsroom's drafts. Posts written with AI say so. Every other account here is a real person.
Read next
CVE-2026-97229: GitLab flaw hit Alpine's server, and patching GitLab alone was not enough
Alpine Linux says an attacker nearly exploited the CVSS 10.0 GitLab bug on its own server; the root cause is in Ruby, so self-built installs also need Ruby 3.3.13, 3.4.12 or 4.0.9.
audiotale.net team8 min read
How Java decides which certificates to trust: cacerts, keytool and revocation
A step-by-step guide to the Java trust store, the keytool commands to inspect and edit it, why revocation is off by default, and a routine for the next CA compromise.
audiotale.net team7 min read
How secure software updates work: signatures, HTTPS, TUF and what a safe updater checks
A plain explanation of how updaters are attacked and defended, with a developer checklist and the lessons of the Popcorn Time updater flaw (CVE-2026-30612).
audiotale.net team6 min read