Description
Regular expression injection occurs when untrusted data is interpreted as all or part of a pattern. An attacker can inject syntax such as .*, alternation (|), groups, or quantifiers to change what a filter or validator matches.
With a backtracking engine, applying a pathological pattern to a sufficiently long, usually nonmatching string can also exhaust CPU resources and cause regular expression denial of service (ReDoS). Metacharacters alone do not establish ReDoS; both a problematic pattern and a triggering subject string are needed.
Potential impact
- Validation bypass: Injected syntax changes the set of accepted or rejected strings.
- Denial of service: Pathological backtracking can occupy a worker's CPU for an extended period.
- Operational cost: Accumulated processing can increase latency, reduce throughput, and raise autoscaling costs.
Remediation
- Prefer fixed patterns maintained by the application. For literal containment, prefix, or suffix checks, use
in,startswith, orendswith. - If untrusted text must become a literal pattern fragment, escape that fragment with
re.escape(...)orregex.escape(...).re.escapeis not for replacement strings passed tosuborsubn. - When supported searches are predefined, use a finite allow-list of complete trusted patterns.
- If users must supply full regular expressions, restrict permitted syntax and both pattern and subject lengths. Use a nonbacktracking engine or an engine-enforced match timeout. A timeout does not prevent validation bypass caused by altered matching semantics.
- Python's standard
reAPI has no per-match timeout. The third-partyregexpackage supportstimeout=for matching operations. Do not assume an HTTP request timeout stops a CPU-bound regular expression. - Review application-defined fixed patterns separately for catastrophic backtracking, including ambiguous nested quantifiers.
Examples
Before
import re
from flask import Flask, request
app = Flask(__name__)
@app.get("/search")
def search():
pattern = request.args.get("p", "") # The user controls the entire pattern
text = request.args.get("t", "")
return "hit" if re.search(pattern, text) else "no"
The user controls the pattern's meaning and can submit syntax that bypasses a filter or causes pathological backtracking with a suitable subject string.
After
from flask import Flask, request, abort
app = Flask(__name__)
MAX_TERM_LENGTH = 100
MAX_TEXT_LENGTH = 10_000
@app.get("/search")
def search():
term = request.args.get("p", "")
text = request.args.get("t", "")
if len(term) > MAX_TERM_LENGTH or len(text) > MAX_TEXT_LENGTH:
abort(400, "search input too long")
return "hit" if term in text else "no"
String search is sufficient here because regular expression features are unnecessary. Choose the length limits for the product's input policy and processing budget; the illustrated values are not universal security constants.
When user input must be a literal within a fixed regular expression, escape only that fragment:
safe_pattern = re.compile(rf"^{re.escape(term)}$")
Usage considerations
The pattern, subject string, and replacement string for sub/subn have different roles. Handle input according to its position, and review fixed application patterns separately for ReDoS.