Bypassing Regex Restrictions
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\nby default - Multiline flag abuse:
/mchanges^and$to per-line re.matchvsre.searchvsre.fullmatchin 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\zfor true end-of-string. The/Dmodifier makes$match only at end-of-string - JavaScript:
.does not match newline. No\z, but$without/mdoes match end-of-string (except some edge cases with\n). The/sflag (dotAll) makes.match newlines - Java:
$matches before trailing newline by default. Use\zfor true end.Pattern.DOTALLmakes.match newlines - Ruby:
^and$are per-line by default (multiline is always on). Use\Aand\zfor string boundaries
Warning
Ruby's default multiline behavior is the most commonly exploited. A Rails application using /^https?:\/\// to validate URLs can be bypassed with javascript:alert(1)\nhttp:// because ^ matches the start of the second line.
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:
- Check anchoring: does it have both
^and$(or\Aand\z)? - Check flags: is
/mor/sset? In Ruby,^$is per-line by default - Try newline injection:
%0aafter valid input, payload on next line - Try trailing newline: valid input +
%0aat the end (bypasses$without\z) - Check decode order: does the regex run before or after URL decoding?
- Try double encoding:
%25xxfor each special character - Check
preg_matchreturn handling: can you trigger a backtracking error?
References
-
Regular-Expressions.info: AnchorsLINK: Regular-Expressions.info: Anchors, www.regular-expressions.info/anchors.html (opens in new tab)
Comprehensive reference on regex anchors, boundary matchers, and engine-specific behavior.
-
PortSwigger: SSRF Defense BypassTOOL: PortSwigger: SSRF Defense Bypass, portswigger.net/web-security/ssrf#circumventing-common-ssrf-defenses (opens in new tab)
Practical examples of bypassing regex-based SSRF filters including IP encoding tricks.