Regular expression injection

Regular expression injection

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, or endswith.
  • If untrusted text must become a literal pattern fragment, escape that fragment with re.escape(...) or regex.escape(...). re.escape is not for replacement strings passed to sub or subn.
  • 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 re API has no per-match timeout. The third-party regex package supports timeout= 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

python
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

python
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:

python
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.

References