Safeguard
Application Security•

CWE-1333 Inefficient Regular Expression Complexity (ReDoS)

A regular expression that backtracks badly can pin a CPU with one short input. How ReDoS works, how to spot risky patterns and what to change.

Safeguard Security Team
5 min read

CWE-1333, Inefficient Regular Expression Complexity, is the weakness behind ReDoS. A regular expression with worst-case exponential or otherwise inefficient complexity meets an input built to trigger it, and the CPU stays busy for far longer than anyone planned.

Any service that runs a regex on data it does not control can be hit: web servers parsing headers, markdown renderers, user-agent parsers, version control tools. In a single-threaded runtime one slow match can stop everything else.

How it happens

Many regex engines use backtracking. When a pattern can match the same text in several ways, the engine tries each way before it concludes there is no match. If the pattern has overlapping or nested quantifiers, the number of ways can grow explosively with input length.

A classic example in JavaScript:

// Vulnerable: nested quantifiers cause catastrophic backtracking
const re = /^(a+)+$/;

// Matches quickly
re.test("aaaaaaaaaaaaaaaaaaaaaaaa");

// Fails after enormous work, because the final "!" forces the engine
// to try every way of splitting the a's between the groups
re.test("aaaaaaaaaaaaaaaaaaaaaaaa!");

Add a few more a characters and the second call takes noticeably longer each time. The input is tiny. The cost is not.

The record lists observed CVEs where this appeared in real software:

  • CVE-2020-5243: a server allows ReDoS through crafted User-Agent strings, due to overlapping capture groups that cause excessive backtracking.
  • CVE-2021-21317: an npm user-agent parser prone to ReDoS for the same reason, overlapping capture groups.
  • CVE-2019-16215: a markdown parser uses an inefficient regex on a message, so a user can cause CPU consumption and delay processing of other messages.
  • CVE-2019-6785: a long string in a version control product allows denial of service because of an inefficient regex.

Two of those four are parsers for headers or text, which tells you where to look first: anything that takes user-agent strings, markdown, version strings or free text.

What an attacker gains

The consequence in the record is availability: a crafted input makes the regex use excessive backtracking and CPU spikes. There is no data theft here. The cost is that a small number of cheap requests can make a service slow or unresponsive for everyone, which is why ReDoS is also a cost-efficient attack.

How to find it

MITRE says static analysis (SAST) can find some instances by modeling data flow, and pattern analysis can flag risky regex shapes.

Practical checks:

  • Search for nested quantifiers such as (a+)+, (a*)* and alternations with overlapping branches like (a|aa)+.
  • Review every regex that runs on data from a request, a file or a network message, especially ones that are anchored at both ends.
  • Test candidate patterns with long inputs that almost match, and time them.
  • Check your dependencies. In the observed CVEs the vulnerable regex sits inside a library, not in application code. A scan of your lockfile against known advisories will surface those, and the free in-browser scanner at gold.safeguard.sh/scan (no login, file stays on your machine, up to 100 dependencies) is a quick first look. Reachability analysis then tells you whether your code actually calls the affected function.

How to fix it

From the record, with the effectiveness MITRE gives:

  1. Use regular expressions that do not support backtracking, for example by removing nested quantifiers. Rated high, and MITRE notes it is one of the few effective options when users supply their own regular expressions.
  2. Do not use regular expressions on untrusted input. If you must, avoid backtracking in the expression. Rated high.
  3. Limit the length of the input the regex will process. Rated moderate.
  4. Set backtracking limits in the regex implementation configuration, such as PHP's pcre.backtrack_limit, and consider time limits on the process. Rated moderate.

A rewrite and a length cap for the earlier example:

// Safer: no nested quantifier, plus an input length cap
const re = /^a+$/;

function isAllA(input) {
  if (input.length > 256) return false;
  return re.test(input);
}

The rewritten pattern accepts the same strings as the original and has linear behavior.

Some engines, such as RE2 and the Rust regex crate, guarantee linear time matching by design and do not support features that need backtracking. If your language offers one, using it for untrusted input removes this class of bug.

Try it yourself

Pick a regex from your own codebase that runs on request data. Build a string that almost matches, double its length three times, and time each run. Linear growth is fine. Rapid growth means you have found one.

Quick check

Why does a short input cause such a long run time? With nested or overlapping quantifiers, the number of ways to match grows exponentially with length, and a failed match makes the engine try them all.

What if users can submit their own regular expressions? MITRE says using an engine or pattern style without backtracking is one of the few effective solutions in that case.

Is limiting input length enough? It helps, rated moderate, but the better fix is a pattern that cannot backtrack catastrophically.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.