Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Regular Expressions  Module 11 – Performance, ReDoS and security

Regex with untrusted patterns and input

Design a search feature safe against user patterns and user input: linear engines, deadlines, match limits, input caps, escaping and patched engines.

  • Advanced
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Design a search feature that accepts user patterns without risking a slow-regex denial of service
  • Recognise when a user pattern is code you must not run on a backtracking engine
  • Choose between a linear engine, a hard deadline, engine limits and an input cap for a feature
  • Protect a feature by escaping user text that is not a pattern and by keeping the engine patched

Before you start

What you’ll learn · Before you start

What you will learn

  • Design a search feature that accepts user patterns without risking a slow-regex denial of service
  • Recognise when a user pattern is code you must not run on a backtracking engine
  • Choose between a linear engine, a hard deadline, engine limits and an input cap for a feature
  • Protect a feature by escaping user text that is not a pattern and by keeping the engine patched

Before you start

On this page

A search box that compiles whatever a visitor types is running their code on your server. Give your own box, backed by Node’s default engine, the pattern ^(a+)+$ and a line of 34 letters a followed by a !, which it cannot match, and the match does not finish: it is still running after three seconds, on the one thread that serves every other request.

Untrusted regex reaches your code two ways: a user supplies the untrusted pattern: A regular expression that comes from outside your team, such as one a visitor types into a search or filter box. It must be treated as code, because the wrong pattern can make a backtracking engine run for a very long time., or a user supplies the input your own pattern runs on. This lesson designs one search feature that is safe against both. You will treat a user pattern as code, choose between a linear engine, a hard deadline, engine limits and an input cap, escape text that was never meant to be a pattern, and keep the engine patched so its own bugs do not become yours.

Two ways untrusted regex reaches your code

The first question to ask of any regex that touches the outside world is which part came from outside. There are only two answers, and they need different defences. Either the pattern is untrusted, as in a search box, a filter builder or a product that lets customers write their own alert rules; or the pattern is yours and the text is untrusted, as in a form field, a request header, an uploaded file or a log line. The table names each case and the defence that fits it:

Where the regex comes from What is untrusted The defence that fits
A user types the pattern (search, filters, alert rules) the pattern a linear engine, or a deadline
A user supplies the input your pattern runs on (forms, headers, files) the text an input cap, a safe pattern, escaping

Both cases share one failure: a slow match that blocks the thread, a denial of service from a regex, which catastrophic backtracking and ReDoS covers in full. The defences below each remove or cap that risk in one of the two cases, and a real feature combines several.

Checkpoint

Check: The two threat cases

  1. One question A form field is checked on the server with one of your own patterns. Which threat case is this?

    Choose one answer.

    Show the answer: One question

    Answer: Users supply the input your pattern runs on

    Untrusted regex arrives two ways: a user types the pattern, or a user supplies the text your own pattern runs on. A validated form field is the second case, so the defence is a length cap and a safe pattern, not a linear engine.

A user’s pattern is code you agreed to run

When a visitor types a pattern, you are not reading data; you are accepting a tiny program and promising to run it. Most such programs are harmless, but you cannot assume so, because one pattern is enough. The program below hands a pattern a visitor might type, ^(a+)+$, to Node’s default engine on a line of 34 letters a and a final !. A linear engine would answer false at once; the default engine does not answer at all before the session stops it:

Node's default engine on a user pattern, stopped after 3 s Python · user-pattern-cost/cost.py
"""A user types the pattern ^(a+)+$ into a search box, and someone later searches a
line of 34 letters a followed by one that cannot match. On Node's default engine
the match keeps going; this session stops the program after three seconds and says
so, instead of printing a time that depends on the computer."""

from v8_session import run

run([], r'''
const userPattern = /^(a+)+$/;          // a pattern a visitor typed
const line = "a".repeat(34) + "!";       // a line it cannot match
console.log(userPattern.test(line));     // never reaches here in time
''', limit=3)

Output

$ node probe.mjs
(stopped after 3 s: the program was still running)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cost.py

The output is the whole point: the match is still running when the session ends it, because on a backtracking: How a backtracking engine reacts to a failure: it returns to its most recent choice point, restores the text position it had there, and tries the next alternative. More in “How an engine finds a match, step by step” engine this shape tries a number of ways to split the run that grows explosively with the line length. Catastrophic backtracking and ReDoS names the four shapes that do this; here it is enough to know that a user can type one, so a user pattern needs a bound on how long it may run before you let it near a request thread.

Key idea

Running a pattern a user typed is running untrusted code: on a backtracking engine a small pattern can block the thread for a very long time, so a user pattern needs a time bound, not just a successful compile.

Checkpoint

Check: A user pattern is code

  1. One question A search box compiles whatever a visitor types and runs it on Node's default engine. Why is that a risk?

    Choose one answer.

    Show the answer: One question

    Answer: A pattern such as (a+)+ can make the match run far longer than the request should take

    Running a pattern a visitor typed is running their code. On a backtracking engine, a nested quantifier on a non-matching line can block the thread, which is why a user pattern needs a linear engine or a hard deadline.

A linear engine takes the risk away

The cleanest fix for an untrusted pattern is an engine that no pattern can push past linear time. A RE2: A regular-expression library and syntax from Google whose primary guarantees include match time linear in the input, which is why it leaves out backreferences and lookaround. Go's regexp package accepts its syntax. More in “Regex in Go, RE2 and re2js”-style engine follows every path through the pattern at once, so its match time is linear in the length of the text whatever the pattern does. Go’s regexp package is one, and it checks every pattern when it compiles it. The program below accepts a few filters a visitor might save, refuses the two that need features RE2 does not have, and then runs the nested quantifier ^(a+)+$ on a line of 100,000 letters a and a final !, which it cannot match:

Go 1.27.1: refusing unsafe user filters, then a bounded match Go · go-filters/filters.go
// A search service lets visitors save their own filter patterns. Go's regexp
// accepts only RE2 syntax, so a pattern that would need backtracking is refused
// when the filter is saved, not while a request runs. The refusal is the guard.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func saveFilter(p string) {
	if _, err := regexp.Compile(p); err != nil {
		fmt.Printf("refused   %-14s %v\n", p, err)
		return
	}
	fmt.Printf("accepted  %-14s compiles\n", p)
}

func main() {
	// Filters a visitor might type. Two of them need features RE2 does not have.
	for _, p := range []string{
		`error|warn`,  // plain alternation
		`\b\d{3,5}\b`, // a bounded number
		`(\w+) \1`,    // a backreference
		`(?=.*fail)`,  // a lookahead
		`^(a+)+$`,     // a nested quantifier
	} {
		saveFilter(p)
	}

	// A saved filter run on a long line that cannot match. RE2 answers within a
	// bound it fixes before it starts, so no input can make this search slow.
	re := regexp.MustCompile(`^(a+)+$`)
	line := strings.Repeat("a", 100000) + "!"
	fmt.Printf("match on %d characters: %v\n", len(line), re.MatchString(line))
}

Output

accepted  error|warn     compiles
accepted  \b\d{3,5}\b    compiles
refused   (\w+) \1       error parsing regexp: invalid escape sequence: `\1`
refused   (?=.*fail)     error parsing regexp: invalid or unsupported Perl syntax: `(?=`
accepted  ^(a+)+$        compiles
match on 100001 characters: false

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run filters.go

Two filters are refused when they are saved, not while a request runs: the backreference (\w+) \1 with invalid escape sequence, and the lookahead (?=.*fail) with invalid or unsupported Perl syntax. The nested quantifier ^(a+)+$ compiles and answers false on 100,001 characters, because RE2 runs even that shape in linear-time matching: A guarantee that, for a fixed pattern, the time a match takes grows at most in proportion to the length of the text, whatever the text holds. Go's regexp gives it by following every path of the pattern at once. More in “Regex in Go, RE2 and re2js”. The cost is the two refusals: a non-backtracking engine: A regex engine that matches without backtracking, so its run time stays proportional to the length of the input. .NET's RegexOptions.NonBacktracking is one; in exchange it refuses lookaround, backreferences, atomic and balancing groups, conditionals and the start anchor \G. More in “Regex in Java, .NET, Ruby and Rust” refuses a backreference: A pattern element that names a previously captured group and matches exactly the text that group holds at that moment, written \1 for the first group, \k<name> or (?P=name) for a named one depending on the flavour. More in “Backreferences: matching the same text again”, and RE2 refuses a lookaround: A family of zero-width assertions that test the text to one side of the current position without consuming it: lookahead looks forward and lookbehind looks back. A pattern can require or forbid what surrounds a match this way. More in “Lookahead: conditions without consuming text” as well, so a feature that needs one cannot use this engine. Backtracking engines versus automata engines explains why and covers re2js, a JavaScript port of RE2, and Regex in Java, .NET, Ruby and Rust covers Rust’s regex crate and .NET’s NonBacktracking option.

Read each platform’s own terms before you hand it visitor patterns, though. A linear engine’s time still grows with the size of the pattern as well as the text, so Rust’s documentation advises keeping patterns from outside short and setting a small limit on their compiled size. Microsoft says .NET’s NonBacktracking option and its match timeouts protect patterns your team wrote from expensive input and are no security boundary against hostile patterns; for searches typed by users it suggests a restricted search syntax or Regex.Escape instead of compiling what they typed.

V8 has an experimental linear engine too, reached through Node’s flags. The run below turns it on two ways: a fallback after too much backtracking, and the l flag, which runs a pattern on the linear engine from the start and refuses one it cannot run there:

Node 24.21.0: V8's experimental linear engine, two ways Python · v8-linear/linear.py
"""V8 has an experimental linear-time engine, reached two ways through Node's V8
flags. These flags are experimental: the behaviour here is Node 24.21.0's, not a
promise about every release, so test it before you rely on it.

The first flag moves a match that backtracks too often onto the linear engine. The
second turns on the l flag, which runs a pattern on the linear engine from the
start and refuses one that cannot run there, such as a backreference."""

from v8_session import run

# Fall back to the linear engine after too many backtracks.
run(["--enable-experimental-regexp-engine-on-excessive-backtracks"], r'''
const line = "a".repeat(34) + "!";
console.log(/^(a+)+$/.test(line));
''', limit=3)

# The l flag: linear from the start, and a refusal for a backreference.
run(["--enable-experimental-regexp-engine"], r'''
const line = "a".repeat(34) + "!";
console.log(/^(a+)+$/l.test(line));
try {
  new RegExp("^(a+)\\1$", "l");
} catch (e) {
  console.log(e.name + ": " + e.message);
}
''', limit=3)

Output

$ node --enable-experimental-regexp-engine-on-excessive-backtracks probe.mjs
false
$ node --enable-experimental-regexp-engine probe.mjs
false
SyntaxError: Invalid regular expression: /^(a+)\1$/l: Cannot be executed in linear time

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 linear.py

The fallback flag answers false at once on the same shape that hung a moment ago, and the l flag does too; a backreference under l is refused with Cannot be executed in linear time. These are experimental flags, so test them on your own patterns and your own Node version before you rely on them rather than treating them as settled.

Key idea

A linear-time engine ends the explosive case of a slow user pattern, because its time stays linear in the text whatever the pattern and grows only with the pattern’s own size, at the price of the features it drops: backreferences always, and lookaround in RE2, Go, Rust and .NET.

Checkpoint

Check: A linear-time engine

  1. One question What does a linear-time engine such as RE2 refuse in exchange for its time guarantee?

    Choose one answer.

    Show the answer: One question

    Answer: Backreferences and lookaround

    RE2 follows every path at once, so it leaves out a backreference, which must remember text of any length, and lookaround, which would break its time bound. A pattern that uses either will not compile.

A hard deadline when the engine still backtracks

Sometimes you cannot change the engine: the feature needs lookaround, or the platform has no linear option. Then the next defence is a hard deadline. Run the match somewhere you can stop, and stop it when it passes a time limit, so a slow match cannot block the thread that serves other requests. In Node that place is a worker thread. The program runs a user pattern in a worker and terminates the worker at a deadline; it prints the outcome, never a measured time, because a real runaway would take far longer than the limit:

Node: a user pattern in a worker, stopped at a 200 ms deadline JavaScript · safe-search-worker/worker.mjs
// A hard deadline when the engine can still backtrack: run the visitor's pattern
// in a worker thread, and stop the worker when it runs past its deadline. The main
// thread stays free to answer every other request. The outcome is printed, never a
// measured time, because a real runaway match would take far longer than this.
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';

if (!isMainThread) {
  // In the worker: compile and run the visitor's pattern. On a line it cannot
  // match this call does not return before the deadline.
  const re = new RegExp(workerData.pattern);
  parentPort.postMessage(re.test(workerData.line));
} else {
  const DEADLINE_MS = 200;
  const pattern = '^(a+)+$'; // a pattern a visitor typed
  const line = 'a'.repeat(42) + '!'; // a line it cannot match
  const worker = new Worker(new URL(import.meta.url), { workerData: { pattern, line } });
  const timer = setTimeout(() => {
    worker.terminate();
    console.log(`stopped after ${DEADLINE_MS} ms: the search ran past its deadline`);
  }, DEADLINE_MS);
  worker.on('message', (answer) => {
    clearTimeout(timer);
    console.log(`answered in time: ${answer}`);
  });
}

Output

stopped after 200 ms: the search ran past its deadline

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node worker.mjs

The worker is stopped and the main thread stays available. The MySmartCoPilot regex tester works the same way: it runs the visitor’s pattern in a Web Worker and stops it after 1.5 seconds, with a message that the pattern is too slow on that text.

Regex Tester Its match runs in a Web Worker on your browser's own engine and stops after 1.5 seconds, a worked example of a deadline.

Several engines offer a deadline without a worker. The third-party Python regex module takes a timeout= in seconds, which turns a runaway match into an error you can catch:

Python regex module 2026.9.29: a match timeout that fires, and one that does not Python · regex-timeout/timeout.py
"""The third-party regex module for Python takes a timeout= in seconds: a match that
runs longer raises TimeoutError, which you can catch and turn into a clear reply. A
timeout fires only for the shapes that run long, and only when you set it, so it is
a backstop, not a reason to skip the other defences."""

from regex_session import regex

line = "a" * 34 + "!"  # a line neither pattern can match

# One shape runs long enough for the cap to fire.
try:
    print("(a|aa)+ :", regex.match(r"^(a|aa)+$", line, timeout=1.0))
except TimeoutError as error:
    print("(a|aa)+ : TimeoutError:", error)

# Another answers at once, well inside the same cap.
print("(a+)+   :", regex.match(r"^(a+)+$", line, timeout=1.0))
print("regex", regex.__version__)

Output

(a|aa)+ : TimeoutError: regex timed out
(a+)+   : None
regex 2026.9.29

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timeout.py

One shape raises TimeoutError; the other answers before the limit, so the cap never fires on it. That is the shape of a match timeout: A limit on how long one matching call may run, after which the engine gives up with an error instead of an answer. Python's re has none; the third-party regex module takes timeout= in seconds. More in “Python regex techniques and the regex module” everywhere: .NET takes one on the constructor, and Microsoft lists it among the defences for untrusted input; Ruby has Regexp.timeout, which is nil by default. But a timeout only helps when it is set, and it fires only after the time is spent, so never promise that one will catch every slow pattern: it is a backstop, not a guarantee.

Key idea

A deadline worker: A separate thread or process that runs a match and is stopped when it passes a fixed time limit, so a slow or runaway match cannot block the thread that serves other requests. or a match timeout caps the damage rather than preventing it: the engine still spends the time up to the limit, so pair a deadline with a length cap and a safe pattern where you can.

Checkpoint

Check: A hard deadline

  1. One question Your service must keep a backtracking engine. Which defence stops a runaway match from blocking other requests?

    Choose one answer.

    Show the answer: One question

    Answer: Run the match in a worker you can terminate at a deadline

    When you cannot change the engine, a hard deadline caps the damage: a worker thread or a match timeout stops the match after it has spent the limit, so one slow input cannot block every other request.

Engine limits that count the work, and a cap on the input

A deadline measures time; an engine limit counts steps, which is deterministic and does not depend on the machine’s load. PCRE2 caps the steps of its matching loop at a match limit: A cap on the steps of the matching loop of PCRE2 that turns a runaway match into an error instead of an endless run. pcre2test reports error -47 when it is reached, and PHP returns false. More in “Regex in PCRE2, PHP and Perl”, and the verb (*LIMIT_MATCH=n) at the start of a pattern lowers it. The run below first reports how many steps a worst-case line needs as the line grows, then shows a low cap turning a worst-case line into an error instead of a runaway:

PCRE2 10.49: the match-loop steps, and a low (*LIMIT_MATCH) cap Python · pcre2-limits/limits.py
"""PCRE2 counts the steps of its matching loop and stops at a limit instead of running
away. The subject modifier find_limits_noheap makes pcre2test report the smallest match
and depth limits a subject needs, and the verb (*LIMIT_MATCH=n) at the start of a pattern
lowers the cap, so a worst-case line fails as an error. Subjects are kept short on purpose:
\\[a]{8} stands for eight letters a, and the count climbs fast with the length of the line."""

from pcre2_session import run

run(r'''
# The match-loop steps a worst-case line needs, as the line grows.
/^(a+)+$/
    \[a]{8}!\=find_limits_noheap
    \[a]{12}!\=find_limits_noheap
    \[a]{16}!\=find_limits_noheap

# The same pattern with a low cap: a worst-case line ends as an error, not a runaway.
/(*LIMIT_MATCH=1000)^(a+)+$/
    \[a]{20}!
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
# The match-loop steps a worst-case line needs, as the line grows.
/^(a+)+$/
    \[a]{8}!\=find_limits_noheap
Minimum match limit = 640
Minimum depth limit = 19
No match
    \[a]{12}!\=find_limits_noheap
Minimum match limit = 10240
Minimum depth limit = 27
No match
    \[a]{16}!\=find_limits_noheap
Minimum match limit = 163840
Minimum depth limit = 35
No match

# The same pattern with a low cap: a worst-case line ends as an error, not a runaway.
/(*LIMIT_MATCH=1000)^(a+)+$/
    \[a]{20}!
Failed: error -47: match limit exceeded

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 limits.py

The minimum match limit climbs from 640 at eight letters to 10,240 at twelve and 163,840 at sixteen, sixteenfold for every four more letters, which is why a short worst-case line already shows the risk. With (*LIMIT_MATCH=1000) a line of twenty letters fails with error -47: match limit exceeded, a clear error rather than a hung thread. PHP wraps the same PCRE2 behind pcre.backtrack_limit, and reports it through a return value a caller must read carefully:

PHP 8.5.11: pcre.backtrack_limit and preg_last_error_msg PHP · php-backtrack-limit/limit.php
<?php
// PHP's PCRE caps backtracking at pcre.backtrack_limit. Past the cap, preg_match
// returns false (not 0), and preg_last_error_msg says why. A caller that reads only
// 0 or 1 cannot tell a non-match from a cap, so always check for false and the error.
ini_set('pcre.backtrack_limit', '1000');

$line = str_repeat('a', 20) . '!';
$result = preg_match('/^(a+)+$/', $line);
var_dump($result);
echo preg_last_error_msg(), "\n";

// A plain match on the same input still answers and reports no error.
$ok = preg_match('/^a+$/', str_repeat('a', 20));
var_dump($ok);
echo preg_last_error_msg(), "\n";

echo 'PCRE2 ', explode(' ', PCRE_VERSION)[0], "\n";

Output

bool(false)
Backtrack limit exhausted
int(1)
No error
PCRE2 10.44

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php limit.php

Past the limit, preg_match returns false, not 0, and preg_last_error_msg says Backtrack limit exhausted. A caller that reads only 0 or 1 cannot tell a stopped match from a clean no-match, so always check for false and the error. All of these cap the work; none of them caps the input. The cheapest guard of all is an input length cap: A limit on the length of the text, or pattern, a feature will accept, checked before matching begins, which shrinks the worst case of every pattern. A short input can still be slow on a backtracking engine, so a cap works beside a safe pattern or a deadline. checked before matching: OWASP’s input validation guidance is to bound the input’s length before matching, and to check on the server as well as in the browser, because a request can skip the browser. A cap shrinks the worst case for every pattern, but it cannot make a slow pattern fast: twenty letters were already enough to pass the 1,000-step cap above.

Checkpoint

Check: Engine limits and input caps

  1. One question In PHP, preg_match returns false (not 0) on a line that hits the backtrack limit. What must the caller do?

    Choose one answer.

    Show the answer: One question

    Answer: Check for false and read preg_last_error_msg, separately from a no-match result

    PHP returns false and sets an error when the backtrack limit is reached. A caller that reads only 0 or 1 cannot tell a stopped match from a real no-match, so it must check for false and read preg_last_error_msg.

Escape text that is not a pattern, and keep engines patched

The second threat case, a user supplying text rather than a pattern, has its own defence. When a visitor types search text, you do not want it read as a pattern at all: a dot should be a dot and an asterisk an asterisk. text escaping: Turning a piece of text into a pattern that matches it literally, by escaping every character that would otherwise be special, so that user search text is never read as regex syntax. the text returns a pattern that matches it literally. RegExp.escape does this in JavaScript, and the same text goes in both the browser and Node:

RegExp.escape: a search term matched literally JavaScript · escape-js/escape.mjs
// A visitor types search text, not a pattern. RegExp.escape turns every character
// into one that matches itself, so the search finds that exact text and treats
// 1.5*(x) as six ordinary characters, not as a quantifier and a group.
const term = "1.5*(x)";
const escaped = RegExp.escape(term);
console.log(escaped);
const re = new RegExp(escaped);
console.log(re.test("y = 1.5*(x) + 2"));
console.log(re.test("y = 15xxx"));

Output

\x31\.5\*\(x\)
true
false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node escape.mjs

re.escape: the same term in Python Python · escape-py/escape.py
import re

# A visitor types search text, not a pattern. re.escape makes every character match
# itself, so the search finds the exact text and never reads * or ( as syntax.
term = "1.5*(x) a-b"
escaped = re.escape(term)
print(escaped)
pattern = re.compile(escaped)
print(bool(pattern.search("value 1.5*(x) a-b here")))
print(bool(pattern.search("1 5 x a b")))

Output

1\.5\*\(x\)\ a\-b
True
False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 escape.py

Each turns the term into a pattern where every special character is escaped, so the search finds the exact text and a crafted term cannot reshape the pattern or slow it down. RegExp.escape also writes the leading 1 as \x31: it escapes a first digit or letter in hex, so that, joined after an escape such as \1 in a bigger pattern, the character is not read as part of that escape. Other platforms have the same tool: Go’s regexp.QuoteMeta, PHP’s preg_quote, Java’s Pattern.quote and .NET’s Regex.Escape.

A regex engine is itself software that can have bugs. The PCRE2 advisory GHSA-r9hj-j2rw-4q3m describes an out-of-bounds write in the engine’s JIT matching code, a memory-safety bug in versions 10.48 and earlier that is fixed in 10.49, and it notes that a service which runs only patterns it trusts is not exposed: one more reason to treat a user pattern as code. The response to any such advisory is to update to the fixed version and to find every bundled copy, because a language runtime often ships its own build of the engine. PHP bundles its own PCRE2, and the copy in PHP 8.5.11 here reports version 10.44, older than that fix and not the system’s.

Find every copy of the engine, not just the system one

When an advisory names a fixed version, the engine your language links is often a separate, older build. Check the version your runtime reports (PHP’s PCRE_VERSION, Node’s process.versions) as well as the system package, and update both. Describe the bug by its class and its fixed version; never work out how to trigger it.

Patching is the one defence on this list that no pattern, engine or limit can replace, because the flaw is in the code that runs every pattern. Treat the engine as a dependency with a version you track, and the two threat cases plus a patched engine cover the whole surface a regex exposes.

Key idea

Escape user text with RegExp.escape, re.escape, QuoteMeta, preg_quote or Pattern.quote so it is matched literally, and keep every copy of the engine patched, because the engine’s own bugs become your service’s bugs.

Checkpoint

Check: Escaping user text, patched engines

  1. One question A visitor types the search text 1.5*(x). How should a search box use it in a pattern?

    Choose one answer.

    Show the answer: One question

    Answer: Escape it with RegExp.escape or re.escape so every character is matched literally

    Search text is not a pattern. Escaping it makes every character match itself, so the search finds the exact text and no crafted term can reshape the pattern or slow it down.

Designing one safe search feature

No single defence fits every feature, so the design question is which to combine. The choice turns on three facts: who writes the pattern, whether it needs lookaround or a backreference, and whether your platform offers a linear engine. Answer them in the table, and it marks the defences that fit your case and the ones to drop:

Interactive · answer and compare Which defences does your search feature need? Answer three questions about your feature, and the table marks the defences that fit and the ones to drop.
Who writes the pattern your feature runs?
Does the pattern need lookaround or a backreference?
Does your platform offer a linear-time engine you can use?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four defences you can stack, against three facts about who writes the pattern and the text
Defence Users type the pattern Users type only text Pattern needs lookaround
A linear-time engine What it gives you: a time guarantee Good no pattern a visitor types can run worse than linear in the length of the line Good linear on any text, though escaping is what makes the text itself safe Poor RE2, Go, Rust and .NET refuse lookaround and backreferences, so such a pattern will not compile
A worker with a deadline What it gives you: a time limit Mixed stops a runaway match, but only after the worker has spent the time Mixed caps the cost; for plain text a length cap is cheaper and simpler Good limits the time, not the pattern, so lookaround and backreferences still work
Escaping the search text What it gives you: literal text Poor makes every character literal, so it cannot be a pattern feature at all Good the one defence that lets a visitor type a search term safely Good your own pattern runs, so it may use any feature it needs
A cap on the input length What it gives you: a smaller worst case Mixed caps pattern and line, which bounds a linear engine; a backtracking one can still be slow on a short line Good a short bound keeps even a backtracking match cheap on a text search Good changes nothing about the pattern, so it fits every feature
How the answers decide
  • “Who writes the pattern your feature runs?” Only your team writes it: adds a caveat to A linear-time engine and A worker with a deadline. When your team owns the pattern, the risk is the input: escape the search text and cap its length, and the heavier nets become optional.
  • “Who writes the pattern your feature runs?” A visitor types it: rules out Escaping the search text. Escaping makes every character literal, which removes the pattern feature a visitor came to use.
  • “Who writes the pattern your feature runs?” A visitor types it and “Does the pattern need lookaround or a backreference?” Yes, it needs one: rules out A linear-time engine. A backreference compiles on no linear engine, and lookaround on none of RE2, Go, Rust or .NET, so for such a pattern a deadline and a cap carry the load.
  • “Who writes the pattern your feature runs?” A visitor types it and “Does the pattern need lookaround or a backreference?” No, it stays plain and “Does your platform offer a linear-time engine you can use?” Yes, one is available: adds a caveat to A worker with a deadline. A plain visitor pattern on a linear engine cannot run away, so a deadline is only a backup; keep the length cap, since the time still grows with the size of the pattern.
  • “Does your platform offer a linear-time engine you can use?” No, only a backtracking one: rules out A linear-time engine. No linear engine is on the platform, so every pattern runs on a backtracking one: cap the input, and put a deadline on any pattern a visitor writes.

A linear engine is ruled out the moment a user pattern needs a backreference or lookaround, or the platform has none; escaping is ruled out the moment the user means the text as a pattern; a deadline and an input cap fit almost anywhere, because they limit the cost rather than the features. With the defences chosen, they run in a fixed order on every request, cheapest guard first. Step through one safe search, from the request to a reply or a clear error:

Interactive · stage by stage One safe search request, from arrival to answer Step through a visitor's search as it passes each guard and ends in a reply, with lines or a clear error.
One safe search request, from arrival to answer, step 8 of 8. Showing Request arrives, Cap the length, Escape or compile, Run on a deadline, Reply (lines found or none). In focus: Reply.too longrefusedpast deadlineRequest arrivesterm or patternCap the lengthrefuse if too longEscape or compiletext or linear engineRun on a deadlinea worker stops itReplylines found or none

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. A search arrives. A visitor sends a search term or a pattern. Every byte of it is untrusted, so the request meets a guard before any matching begins.
  2. Cap the length first. The cheapest guard runs first. A term or pattern longer than the limit is refused, and a line longer than the line cap is skipped, before the engine sees either.
  3. Too long ends here. An over-long input takes the short branch straight to the reply, an error that names the limit, so the engine never starts on it.
  4. Make the input safe. A search term is escaped so every character is literal; a pattern is compiled on a linear engine, which checks it before it runs on any line.
  5. A refused pattern ends here. A pattern that needs a feature the engine will not take, such as a backreference, is refused with a message that names the feature.
  6. Run under a deadline. The match runs in a worker with a hard stop, a backstop in case a match still runs long, so the main thread is never blocked past the deadline.
  7. Past the deadline ends here. A match still running at the deadline is stopped, and the reply is the same kind of clear error, not a hung request.
  8. Answer the search. A match that finished returns the lines it found, or none. Every path ends at the reply, with an answer or an error the visitor can act on.

The length cap runs first, because it is a plain length check and the cheapest way to reject a huge input before the engine does any work. Only inputs within the limits go on to be escaped (search text) or compiled on a linear engine (a pattern), and the match itself runs under a deadline, so every path ends in an answer or an error the visitor can act on.

Key idea

A safe feature stacks defences in order: cap the length, then escape text or compile a pattern on a linear engine, then run under a deadline, so an over-long or runaway input becomes a clear error and never a blocked thread.

Checkpoint

Check: Designing the feature

  1. One question A search box accepts only search text, never a pattern. Which defences does that case most need?

    Choose one answer.

    Show the answer: One question

    Answer: Escaping the text, plus a length cap on the input

    When a user supplies only text, escaping makes it literal and a length cap bounds the work. The pattern-side defences, a linear engine or refusing fancy features, answer a case this feature does not have.

Practise

The exercise builds the feature this lesson designed: a safeSearch that a visitor can call in a text mode and a pattern mode. It escapes search text, refuses a pattern that holds a backreference, a lookaround or a quantifier inside a quantifier, caps the pattern and the line length, refuses a mode it does not know, and returns every error as a value rather than throwing. The starter returns every line, so the first test fails at once until you add the guards:

Then work through the self-check. It starts from the idea you have shown least and explains every choice as soon as you make it:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Which of these feed untrusted regex into a service? Select all that apply.

    Choose every answer that is right.

    Show the answer: Question 1 of 14

    Answer:

    • A visitor uploads a file your fixed patterns scan
    • A visitor types a pattern into a search or filter box

    Untrusted regex is either a pattern a user supplies or input a user supplies to your pattern. A literal your team wrote, run on your own data, is neither.

  2. Question 2 of 14 An analytics tool lets customers save their own log filters, which a worker then runs against every stored line. Which threat case is this, and what is the first defence?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: Users supply the pattern, so run it in a linear engine or a worker with a deadline

    Saved customer filters are user-supplied patterns. The first defence is to run each on a linear engine, or in a worker with a hard deadline, so no filter can block the worker for long.

  3. Question 3 of 14 Why is "treat a user pattern as code" a useful rule?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: Because running a pattern a user wrote spends your CPU on their instructions, which can be slow by design

    A pattern is instructions for the engine. Running an untrusted one is running untrusted code, so it needs a bound on how long it may take, just as untrusted code needs a sandbox.

  4. Question 4 of 14 A visitor saves the pattern ^(a+)+$, and a later search runs it, on Node's default engine, on a line of 34 letters a followed by a !. What happens?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: The match keeps running far past the request, so the thread is blocked

    On the default backtracking engine this shape is the classic slow case: the work grows explosively with the line length, so the request is blocked. A linear engine or a deadline is what makes it safe.

  5. Question 5 of 14 Which engines run a match in time linear in the input, whatever the pattern?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: RE2 and Go, Rust's regex crate, and .NET's NonBacktracking option

    RE2 (and Go), Rust's regex crate and .NET's NonBacktracking run every path at once, so no input can push them into a slow lane. In the browser, re2js carries the same guarantee.

  6. Question 6 of 14 A visitor filter needs a backreference, but you want to run user patterns on a linear engine. What happens, and what do you do?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: The pattern will not compile on the linear engine, so offer a deadline instead or drop the feature

    A linear engine cannot take a backreference, so the pattern is refused at compile time. Either keep a backtracking engine behind a hard deadline for such filters, or decide the feature does not allow them.

  7. Question 7 of 14 What does a match timeout or a worker deadline give you, and what does it cost?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: It stops a match that runs too long, but only after the engine has spent that time

    A deadline is a backstop. It does not stop a slow match from starting, but it bounds how long one may run, so a single bad input cannot block a thread forever.

  8. Question 8 of 14 In the regex-module run, the same timeout= raised TimeoutError for (a|aa)+ and let (a+)+ answer None. What does that tell you about a match timeout?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: It fires only when a match runs past it, after that time is spent, so it bounds the damage of a slow match

    A timeout must be set, and it only fires once a match has run past it. It is a useful backstop, but never promise it will catch every slow pattern, so pair it with a length cap and a safe engine where you can.

  9. Question 9 of 14 With pcre2test and ^(a+)+$, the minimum match limit was 640 at 8 letters and 10,240 at 12. At 16 letters it was reported as which number?

    Type a number.

    Show the answer: Question 9 of 14

    Answer: 163840

    The count climbs sixteenfold for every four more letters: 640, then 10,240, then 163,840 at 16 letters. That steep climb is why a short worst-case line is enough to show the risk, and why a match limit stops it.

  10. Question 10 of 14 A product lets operators write patterns that a PCRE2 service runs. How do PCRE2's match limit and an input cap work together?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: The input cap bounds the line length first, and (*LIMIT_MATCH=n) turns a worst-case line into an error instead of a runaway

    A length cap lowers the worst case for every pattern, and a match limit stops a line that still backtracks too far, failing with error -47 rather than running away. Together they bound both the input and the work.

  11. Question 11 of 14 What does RegExp.escape (or re.escape) do to a piece of text?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: It returns a pattern that matches the text literally, escaping every character that would be special

    Escaping takes search text and returns a pattern that matches it exactly, so user text is never read as regex syntax. It is the defence for the case where the user supplies text, not a pattern.

  12. Question 12 of 14 A security advisory reports a memory-safety bug in the JIT matching code of the regex engine your service uses, and names the fixed version. What is the right response?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Update to the fixed version, and find every bundled copy of the engine, since a runtime may ship its own

    A regex engine is software that can have bugs. When an advisory names affected and fixed versions, the response is to update and to find every bundled copy, because a language runtime often ships its own build of the engine.

  13. Question 13 of 14 Visitors type patterns that may need lookaround, and you can choose the platform. Which set of defences fits?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: A hard deadline and an input cap, because the RE2-style linear engines refuse lookaround

    When a user pattern needs lookaround, a linear engine is out, so the fit is a backtracking engine behind a hard deadline with an input cap. Escaping is for user text, not for a pattern the user means as a pattern.

  14. Question 14 of 14 In one safe search request, why does the length cap run before escaping or compiling the pattern?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: It is the cheapest guard, so it rejects an over-long input before any matching work begins

    The cap is a plain length check and the cheapest guard, so it runs first: an over-long term, pattern or line is refused before the engine does any work, and only inputs within the limits go on to be escaped or compiled.

Interview questions

Warm-up: name the two ways untrusted regex reaches a service, and the defence that fits each.

Untrusted regex is either a pattern a user supplies, as in a search box, a filter builder or customer-written alert rules, or input a user supplies to a pattern you wrote, such as a form field, a header or an uploaded file. For an untrusted pattern the defences are a linear-time engine, whose time stays linear in the text whatever the pattern, or a hard deadline on a worker or a match timeout when the engine must still backtrack. For untrusted input to your own pattern, cap the length before matching, keep the pattern clear of the slow shapes, and escape any user text that is meant as text rather than as a pattern. Both cases share one failure, a slow match that blocks the thread, so in both the goal is a bound on how long a match may run.

Two design questions take these defences into features this lesson did not build. Decide your first move on each, then open its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Senior to staff

    A security-monitoring product runs customers' saved detection rules in a Node service on eight worker threads, stopping any match after 50 ms. One customer's rule hits the stop on 400 lines a second: every customer's alerts arrive minutes late, and the stopped lines raise no alert. What is wrong, and what do you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A Python importer splits each row of a supplier's CSV with ,(?=(?:[^"]*"[^"]*")*[^"]*$) to find the commas outside quotes, then caps every cell at 256 characters. One supplier's 5 MB export arrives as a single line, and the import worker runs for many minutes. Why, and how do you make the importer safe?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the two ways untrusted regex reaches a service?

    A user supplies the pattern, as in a search box, a filter builder or alert rules, or a user supplies the input your own pattern runs on, such as a form field, a header or an upload. Each needs its own defence.

  2. Why is compiling a pattern a visitor typed like running their code?

    On a backtracking engine a small pattern can block the thread for a very long time: Node's default engine was still running ^(a+)+$ on 34 a's and ! after three seconds. A user pattern needs a time bound, not just a successful compile.

  3. What does a linear-time engine guarantee for user patterns, and what does it refuse?

    Its time stays linear in the text whatever the pattern, which removes the explosive case. It always refuses backreferences, and lookaround too in RE2, Go, Rust and .NET's non-backtracking mode.

  4. What does a deadline worker or a match timeout do, and what does it not do?

    It caps the damage rather than preventing it: the engine still spends the time up to the limit. Pair the deadline with an input length cap and a safe pattern where you can.

  5. Does an input length cap make a slow pattern cheap?

    No. It shrinks the worst case for any pattern by bounding how much text a match can meet, but the pattern itself stays as slow as it was.

  6. How do you match a user's text literally, and why keep the engine patched?

    Escape it with RegExp.escape, re.escape, QuoteMeta, preg_quote or Pattern.quote. Keep every bundled copy of the engine patched as well, because the engine's own bugs become your service's bugs.

  7. In what order does a safe search feature stack its defences?

    Cheapest first: cap the length, then escape the text or compile the pattern on a linear engine, then run under a deadline, so every request ends in an answer or a clear error rather than a blocked thread.

Key takeaways

  • Untrusted regex reaches a service two ways, a user-supplied pattern and user-supplied input, and each needs its own defence, so the first question is always which part came from outside.
  • A pattern a user typed is code you agreed to run, and on a backtracking engine a small pattern can block the thread, so a user pattern needs a bound on its time, not just a successful compile.
  • A linear-time engine removes the explosive case of a slow user pattern, because its time stays linear in the text whatever the pattern, at the cost of refusing backreferences and, in most such engines, lookaround.
  • When the engine must still backtrack, a hard deadline on a worker, a match timeout or an engine limit such as PCRE2’s (*LIMIT_MATCH) caps the damage, and an input length cap shrinks the worst case for any pattern.
  • Escape user text with a tool such as RegExp.escape or re.escape so it is matched literally, and keep every bundled copy of the engine patched, because the engine’s own bugs become your service’s.
  • A safe feature stacks these defences in a fixed order, cheapest guard first, so every request ends in an answer or a clear error rather than a blocked thread.

Terms in this lesson

deadline worker
A separate thread or process that runs a match and is stopped when it passes a fixed time limit, so a slow or runaway match cannot block the thread that serves other requests.
input length cap
A limit on the length of the text, or pattern, a feature will accept, checked before matching begins, which shrinks the worst case of every pattern. A short input can still be slow on a backtracking engine, so a cap works beside a safe pattern or a deadline.
text escaping
Turning a piece of text into a pattern that matches it literally, by escaping every character that would otherwise be special, so that user search text is never read as regex syntax.
untrusted pattern
A regular expression that comes from outside your team, such as one a visitor types into a search or filter box. It must be treated as code, because the wrong pattern can make a backtracking engine run for a very long time.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.