DEV Community

Cover image for Find catastrophic-backtracking regexes before they ship (free checker + API)
Hudson Enterprises
Hudson Enterprises

Posted on

Find catastrophic-backtracking regexes before they ship (free checker + API)

A regex that works correctly under normal inputs can pin a CPU at 100% when a crafted input triggers catastrophic backtracking. The attack class is CWE-1333 — ReDoS, regular expression denial of service. The runtime doesn't matter: Node.js, Python, Java, Ruby, all use backtracking-based engines. What matters is whether the pattern itself creates an exponentially growing search space on a non-matching input.

This tutorial walks through what catastrophic backtracking looks like, how to detect it before it ships, and how to wire the check into a CI/CD gate so it runs automatically on every changeset.

Everything here runs against ReDoScan — a REST API I built for this specific job. There's also a free web checker if you want to paste a regex without writing code. I built both; I'll be upfront about that throughout.


What catastrophic backtracking actually is

Most regex engines work by trying every possible path through the pattern when a match fails. For safe patterns the search space is small — the engine finds a dead-end quickly and moves on. For certain constructs the search space grows exponentially with input length, and a crafted non-matching input can force the engine to explore it all.

Three constructs reliably cause this:

Nested quantifiers(a+)+

The inner a+ can match one or more as. The outer + lets the group repeat. For input aaaaaX, the engine tries every way to distribute the as across iterations before concluding there's no match. The number of combinations is exponential in the length of the input.

Overlapping alternation(a|a)*

Both branches match the same input. The engine explores every combination of which branch matched which character.

Prefix overlap(a|ab)*

The two branches share a prefix. On a non-matching input the engine can't prune the search tree early.

A pattern like ^(\w+)+$ — common in username validators — is nested quantifiers in production clothes. \w+ expands to [a-zA-Z0-9_]+. Same exponential behavior. Input aaaaaaaaaaaaaaaaaaaaaaaaaaaaX will hang the thread.


Step 0 — Try the free checker

Paste any regex at redoscan.hudsonenterprisesllc.com. No account needed. Static analysis runs and returns a risk badge (safe / low / medium / high / critical) with the findings in plain English.

Paste (a+)+$. It comes back high with the finding: "Nested quantifier inside a quantified group. Triggers exponential backtracking on long non-matching inputs."

The API adds an optional dynamic timing layer for deeper checks — covered in Step 3.


Step 1 — Set up the API

Free tier on RapidAPI: 1,500 scans/month, no credit card. Subscribe at the ReDoScan listing and your X-RapidAPI-Key appears in the dashboard within seconds.

export RAPIDAPI_KEY="your_key_here"
Enter fullscreen mode Exit fullscreen mode

Step 2 — Scan one pattern

curl -s -X POST "https://redoscan1.p.rapidapi.com/scan" \
  -H "Content-Type: application/json" \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: redoscan1.p.rapidapi.com" \
  -d '{"pattern": "(a+)+$", "dynamic": false}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "pattern": "(a+)+$",
  "overall_risk": "high",
  "is_redos_vulnerable": true,
  "static_findings": [
    {
      "rule_id": "nested-quantifier",
      "severity": "high",
      "description": "Nested quantifier inside a quantified group. Triggers exponential backtracking on long non-matching inputs.",
      "matched_at": 0
    }
  ],
  "syntax_valid": true,
  "scan_time_ms": 0.5
}
Enter fullscreen mode Exit fullscreen mode

overall_risk gives you the 5-level classification. is_redos_vulnerable is the boolean gate for automated pipelines. static_findings tells you which rule fired and where in the pattern — useful when you need to explain the finding in a PR comment or a security ticket.


Step 3 — Add dynamic timing (optional, deeper)

Static rules catch the canonical constructs in sub-millisecond time — the right default for a batch CI gate. Dynamic timing feeds the pattern adversarial inputs of increasing length, measures how runtime scales, and classifies the growth curve (safe / polynomial / exponential). It catches some patterns that static rules miss.

curl -s -X POST "https://redoscan1.p.rapidapi.com/scan" \
  -H "Content-Type: application/json" \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: redoscan1.p.rapidapi.com" \
  -d '{"pattern": "(a+)+$", "dynamic": true}'
Enter fullscreen mode Exit fullscreen mode

The response adds a dynamic_finding object with per-length timing data (timings_ms) and the growth classification (growth_classification). Use dynamic on a final audit pass or for a single suspicious pattern you want to confirm. For a 200-pattern batch in CI, keep "dynamic": false.


Step 4 — Wire it into a CI gate

The /scan-batch endpoint takes up to 200 patterns in one request — the right tool for a pre-merge gate. Extract every regex literal from a changeset, batch-scan them, fail the build on high or critical.

import os
import sys
import requests

RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]

# These would come from static analysis of the changed files
patterns = [
    "^[a-z]+$",           # safe
    "(a+)+",              # high — nested quantifier
    "^(\\w+)+$",          # high — same pattern, common in validators
    "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$",  # email — likely safe
]

r = requests.post(
    "https://redoscan1.p.rapidapi.com/scan-batch",
    json={"patterns": patterns, "dynamic": False},
    headers={
        "X-RapidAPI-Key": RAPIDAPI_KEY,
        "X-RapidAPI-Host": "redoscan1.p.rapidapi.com",
    },
    timeout=30,
)
r.raise_for_status()
results = r.json()

# Fail on high or critical
high_risk = [x for x in results if x["overall_risk"] in ("high", "critical")]
if high_risk:
    print(f"FAIL: {len(high_risk)} high-risk regex(es) detected:")
    for x in high_risk:
        print(f"  Pattern: {x['pattern']}")
        print(f"  Risk:    {x['overall_risk']}")
        for f in x.get("static_findings", []):
            print(f"  Finding: {f['description']}")
    sys.exit(1)

print(f"PASS: {len(results)} patterns scanned, none high-risk or critical")
Enter fullscreen mode Exit fullscreen mode

One thing worth noting: invalid patterns come back with "syntax_valid": false rather than erroring the whole batch. A malformed pattern won't stop the other 199 from being checked.


Step 5 — Validate against the known-evil corpus

GET /known-evil is a public endpoint — no API key required. It returns a curated corpus of canonical evil patterns, real-world CVE patterns, and safe baselines.

curl -s "https://redoscan1.p.rapidapi.com/known-evil" | python -m json.tool | head -60
Enter fullscreen mode Exit fullscreen mode

Hit this before wiring any gate into production. If a pattern you know is vulnerable comes back safe, something is misconfigured. The corpus also works as a test fixture for anyone building their own tooling on top of the API.


What this is not

Honest caveats, because someone will ask:

  • Not a full SAST suite. It does one thing — ReDoS / regex denial-of-service detection. No SQL injection, no XSS, no secrets. Use a full SAST platform for that; this is the ReDoS check you run without licensing one per seat.
  • Not a guarantee. safe means no static rule fired and — if dynamic was enabled — no pathological growth was observed on adversarial inputs. It does not mean the pattern is invulnerable to every possible input. Real-world CVE patterns can carry non-obvious vulnerabilities that static rules and timing tests both miss. Your own application security review still applies.
  • Not a regex linter. It scores denial-of-service risk, not style or correctness. [A-Za-z]+ can be written as \p{L}+ — ReDoScan doesn't care about that. The score is about whether the pattern can be weaponized against your server.

Pricing

Tier Price Scans/month
BASIC $0 1,500
PRO $9/mo 10,000
ULTRA $49/mo 100,000
MEGA $199/mo 1,000,000

No credit card on the free tier. Subscribe at the RapidAPI listing. Every call needs X-RapidAPI-Key and X-RapidAPI-Host: redoscan1.p.rapidapi.com — that's the full auth story.


Questions or issues

support@hudsonenterprisesllc.com — same-business-day response, async only. Include your RapidAPI key prefix (first 8 chars), the endpoint, and the request body if you're reporting an issue.


Built by Hudson Enterprises LLC, an Indiana software studio.

Top comments (0)