Skip to content

Bypassing Regex Restrictions

5 min read Created Sep 13, 2026 Last updated Sep 13, 2026

Web applications use regular expressions to validate and sanitize user input: URL patterns, filename filters, IP address checks, and input validation rules. Flawed regex patterns create bypass opportunities that let an attacker pass malicious input through what appears to be a robust validation layer. The most common mistakes are anchoring errors, newline injection, incorrect flag usage, and differences between regex engines. For bypassing command injection filters specifically, see command injection bypasses. For XSS filter evasion, see XSS filter bypass.

Bypass techniques covered on this page:

  • Anchoring issues: missing ^ or $, trailing newline behavior
  • Newline injection: . does not match \n by default
  • Multiline flag abuse: /m changes ^ and $ to per-line
  • re.match vs re.search vs re.fullmatch in Python
  • PCRE vs POSIX differences
  • Double URL encoding and null byte bypasses

Anchoring Issues

The most common regex bypass. When a pattern uses ^ without $ (or vice versa), the regex only validates part of the input. The attacker appends or prepends payload after the validated portion.

Missing End Anchor

Pattern: /^[a-zA-Z0-9]+/

Intent: allow only alphanumeric input. Reality: validates that the string starts with alphanumeric but does not check what follows.

# PHP example: missing $ anchor
if (preg_match('/^[a-zA-Z0-9]+/', $input)) {
    // Passes validation
    system("lookup " . $input);
}

// Bypass: input = "validuser; id"
// Regex matches "validuser" at the start, ignores "; id"

Missing Start Anchor

Pattern: /[a-zA-Z0-9]+$/

Intent: alphanumeric only. Reality: validates the end but allows anything before it.

// Bypass: input = "; cat /etc/passwd; echo validuser"
// Regex matches "validuser" at the end

Dollar Sign Does Not Mean End of String

In most regex engines, $ matches the position before a trailing newline, not strictly the end of the string. This means /^safe$/ matches safe\n (with a trailing newline). In PHP:

// PHP: $ matches before trailing newline
preg_match('/^[a-z]+$/', "safe\n");  // Returns 1 (match!)
preg_match('/^[a-z]+$/', "safe\nmalicious");  // Returns 0

// Fix: use \z instead of $ for true end-of-string
preg_match('/^[a-z]+\z/', "safe\n");  // Returns 0 (no match)

Exploitation: if an application checks a filename with /^[a-z]+\.txt$/ and then uses it in an include(), submitting safe.txt\n passes the regex. Depending on how the language handles the null byte or newline in the file path, this may allow path traversal or inclusion of unexpected files.

Newline Injection

The dot character (.) matches any character except newline (\n) by default in most regex engines. Injecting a newline into the input breaks the pattern, causing the regex to match only the part before the newline while the rest passes through unvalidated.

# Pattern intended to block "script" anywhere in input
if not re.search(r'script', user_input):
    render_html(user_input)

# Bypass: inject a newline in the middle
# input = "scr\nipt" - regex does not find "script" as a contiguous match
# But the HTML renderer may ignore the newline and execute it

URL-encoded newline: %0a (LF) or %0d%0a (CRLF). When the application decodes URL encoding before applying the regex, the newline is present in the string being matched.

Another angle: if a regex validates an entire block of text but . does not match newlines, the regex effectively only validates the first line. Payload on line 2 goes unchecked.

Multiline Flag Abuse

The multiline flag (/m in most engines, re.MULTILINE in Python) changes the behavior of ^ and $: instead of matching the start/end of the entire string, they match the start/end of each line. If a developer uses the multiline flag thinking it enables multi-line input support but their regex uses ^...$, the pattern validates each line independently rather than the whole input.

# Python: multiline flag makes ^ and $ per-line
import re

pattern = re.compile(r'^[a-z]+$', re.MULTILINE)

pattern.search("safe\n<script>alert(1)</script>")  # Matches! ("safe" on line 1)
pattern.search("<script>alert(1)</script>")  # No match

# Without MULTILINE:
re.search(r'^[a-z]+$', "safe\n<script>alert(1)</script>")  # No match

The attacker puts a valid value on the first line and their payload on a subsequent line. The regex matches the first line and the application processes the entire multi-line input.

Python: match vs search vs fullmatch

Python has three regex matching functions with different behavior, and using the wrong one creates bypasses:

import re

input_str = "admin' OR '1'='1"

# re.match: anchored at START only, not end
re.match(r'[a-z]+', input_str)     # Matches "admin" (ignores rest)

# re.search: finds pattern ANYWHERE in string
re.search(r'^[a-z]+$', input_str)  # No match (correct rejection)

# re.fullmatch: anchored at BOTH start and end (Python 3.4+)
re.fullmatch(r'[a-z]+', input_str) # No match (correct rejection)

# Common mistake: using re.match without $ anchor
if re.match(r'[a-z]+', input_str):  # Returns truthy match object!
    query = f"SELECT * FROM users WHERE name='{input_str}'"  # SQLi

re.match only checks the beginning of the string. Code that uses re.match(pattern, input) without a $ anchor in the pattern is equivalent to a regex with ^ only. Use re.fullmatch() for input validation.

PCRE vs POSIX Differences

PHP supports both PCRE (preg_match) and POSIX (ereg, deprecated) regex. Other languages have their own engine quirks:

  • PHP PCRE: $ matches before trailing newline by default. Use \z for true end-of-string. The /D modifier makes $ match only at end-of-string
  • JavaScript: . does not match newline. No \z, but $ without /m does match end-of-string (except some edge cases with \n). The /s flag (dotAll) makes . match newlines
  • Java: $ matches before trailing newline by default. Use \z for true end. Pattern.DOTALL makes . match newlines
  • Ruby: ^ and $ are per-line by default (multiline is always on). Use \A and \z for string boundaries

URL Encoding and Double Encoding

If the regex runs on the raw input before URL decoding, encoded characters bypass character-class restrictions:

Input:   test%27+OR+1%3d1--
Decoded: test' OR 1=1--

# If regex checks for single quotes on raw input:
preg_match('/\'/', $raw_input)  // No match (sees %27, not ')
# Application then URL-decodes and uses in query: SQLi

Double encoding works when the application decodes twice but the regex only sees the first decode:

Original:       ../etc/passwd
Single encoded: %2e%2e%2fetc%2fpasswd
Double encoded: %252e%252e%252fetc%252fpasswd

# WAF/regex checks after first decode: sees %2e%2e%2f (no match for ../)
# Application decodes again: sees ../etc/passwd

Null Byte Injection

In older PHP versions (before 5.3.4) and some C-based regex engines, a null byte (%00) terminates the string for the regex engine but not for subsequent file operations:

# PHP < 5.3.4
$file = $_GET['file'];  // "shell.php%00.jpg"

if (preg_match('/\.jpg$/', $file)) {
    // Matches: regex sees "shell.php\0.jpg", matches .jpg
    include($file);  // include sees "shell.php" (truncated at null byte)
}

Modern PHP versions fix this, but the technique still works in some compiled applications and CGI environments where C string handling truncates at null bytes.

Backtracking Limit (ReDoS as Bypass)

PHP's PCRE engine has a backtracking limit (pcre.backtrack_limit, default 1000000). Crafted input that exceeds this limit causes preg_match to return false (error) rather than 0 (no match). If the application checks if (!preg_match(...)), a ReDoS payload causes the regex to fail, and the negative check interprets the failure as a non-match, passing the validation:

// Vulnerable pattern: catastrophic backtracking
$pattern = '/^(a+)+$/';
$input = str_repeat('a', 30) . '!';

$result = preg_match($pattern, $input);
// $result = false (backtracking limit exceeded)

if (!$result) {
    // Developer intended: "no match, input is safe"
    // Reality: regex errored out, input was NOT validated
    process_input($input);
}

Fix: always check preg_match() === 0 (strict comparison to integer zero) instead of !preg_match() to distinguish between no-match and error.

Testing Methodology

When you find a regex-based validation during source code review, test these bypass categories in order:

  1. Check anchoring: does it have both ^ and $ (or \A and \z)?
  2. Check flags: is /m or /s set? In Ruby, ^$ is per-line by default
  3. Try newline injection: %0a after valid input, payload on next line
  4. Try trailing newline: valid input + %0a at the end (bypasses $ without \z)
  5. Check decode order: does the regex run before or after URL decoding?
  6. Try double encoding: %25xx for each special character
  7. Check preg_match return handling: can you trigger a backtracking error?

References