Safeguard
Application Security•

CWE-625 Permissive Regular Expression: Anchor Your Patterns

A regex that matches a substring instead of the whole value lets bad input through. See the common mistakes, a real exploited case and how to fix them.

Safeguard Security Team
5 min read

CWE-625, Permissive Regular Expression, is what you get when a pattern does not sufficiently restrict the values it accepts. The check runs, it passes, and it passes for input the author never meant to allow. MITRE lists Perl and PHP as the platforms, though the mistake shows up in any language with regular expressions.

If you have ever written a validation regex and tested it only with good input, this one is for you.

How it happens

Most regex engines look for a match anywhere in the string unless you tell them not to. A pattern like [a-z0-9]+ does not mean "the whole value is letters and digits". It means "somewhere in the value there is a run of letters and digits". An attacker can append anything after that run.

MITRE names the common errors:

  • Not marking the beginning and end of the target string.
  • Using wildcards such as .* instead of acceptable character ranges.
  • Others, such as checking that at least one character is legal instead of all of them.
import re

# Vulnerable: matches a substring, so "abc; rm -rf /" passes
if re.search(r"[a-z0-9]+", value):
    run(value)
# Fixed: anchored and limited to the intended characters
if re.fullmatch(r"[a-z0-9]{1,32}", value):
    run(value)

The MITRE page lists four observed cases that show how this plays out:

  • A regexp in an EXIF processor did not correctly determine where a string ends, enabling eval injection, and the chain was exploited in the wild per CISA KEV (CVE-2021-22204).
  • A .* pattern led to static code injection.
  • A username inserted into a regexp resulted in a partial comparison, so the wrong database entry was updated when one username was a substring of another.
  • A regexp meant to verify that all characters are legal only checked that at least one was legal, enabling file inclusion.

Notice the pattern. The regex is rarely the end of the story. It is the guard in front of something dangerous, such as an eval call, an include statement or a database update. When the guard is loose, the dangerous operation receives input it should never see.

What an attacker gains

The consequence MITRE lists is bypassing a protection mechanism. The effect then depends on what the regex was protecting. In the examples that meant code injection, file inclusion and altering a record that belonged to a different user.

That makes this weakness a good example of why a finding's severity depends on context. A permissive pattern in front of a display label is a nuisance. The same pattern in front of a command, a path or a query is a way in.

How to find it

MITRE lists automated static analysis (SAST) as a way to find some instances, by modeling the flow of data from input to the sensitive operation.

You can also check by hand. Search your code for validation regexes and ask three questions of each:

  1. Does it start with ^ or use a full-match function, and does it end with $ or the equivalent?
  2. Does it use . or .* where a specific character class would do?
  3. Does it test all characters, or only whether one acceptable character exists?

Then test with hostile input. Take a valid value and add a suffix, a prefix and a newline. If the validation still passes, the pattern is permissive.

Be careful with multiline behavior. In some engines $ also matches before a trailing newline, so an attacker can append a line break and more content. Check your language's documentation for the exact rules, and prefer a full-match function when one exists.

Click through it at your own pace, or open it full screen.

How to fix it

MITRE's mitigation is to mark the beginning and ending of the string pattern when applicable, such as /^string$/ in Perl. Beyond that, a few habits help:

  • Prefer a character class that lists exactly what is allowed over a wildcard.
  • Set length limits in the pattern or next to it.
  • Use the full-match function of your language when available.
  • Do not rely on the regex alone. If the value goes into a command, query or path, use the safe API for that sink as well, such as parameterized queries.
  • Add tests with near-miss inputs: valid value plus a suffix, a prefix, an embedded null byte, a newline.

One more point from the examples: the username case. If you build a pattern from a user's name, the name is data, not syntax. Escape it, and anchor the pattern so that "bob" cannot match "bobby".

Quick check

Why does a pattern like [a-z0-9]+ let bad input through?

Without anchors it only needs a match somewhere in the string. The rest of the value can be anything.

What is MITRE's basic fix?

Mark the beginning and end of the string in the pattern, for example /^string$/ in Perl.

Why did CVE-2021-22204 matter beyond the regex?

The regex failed to find where a string ends, which enabled eval injection. A loose guard let hostile input reach a dangerous call.

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.