Regular Expression without Anchors
Description
Regular Expression without Anchors is an input validation vulnerability where software uses a regular expression to validate or filter input but fails to include anchors (^ for start, $ for end) that constrain the match to the entire input string. Without anchors, the regex matches if the pattern appears anywhere within the input, allowing malicious data before or after the matched portion to bypass validation. This is particularly dangerous when regular expressions are used for allowlist validation, as attackers can inject malicious content alongside valid-looking patterns.
Risk
Unanchored regular expressions provide false security. When used for validation, they appear to work correctly for normal input but fail to block crafted malicious input. For path validation, attackers can include "../" traversal sequences before or after the matched pattern. For IP address validation, attackers can prefix addresses with octal or hexadecimal representations that change their meaning. For email validation, attackers may inject additional content. The consequences depend on what the unanchored regex protects: path traversal, injection attacks, authentication bypass, or other security violations become possible. The vulnerability is subtle and easily overlooked during code review.
Solution
Always use anchors when regular expressions validate entire input strings. Use ^ at the start and $ at the end of patterns intended to match complete strings. Understand what your regex will and won't match—test with malicious inputs containing extra characters before and after the expected pattern. Consider using dedicated validation libraries instead of custom regex patterns. When using regex for filtering rather than validation, ensure that unmatched portions are properly handled. Use regex testing tools to visualize exactly what portions of test strings match. In some languages, use methods that implicitly anchor (like Java's matches() vs find()).
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Bypass Protection Mechanism - Unanchored regex fails to block malicious input that includes valid-looking portions. |
| Integrity | Scope: Integrity Modify Application Data - Malicious data passes through validation and affects application behavior. |
| Confidentiality | Scope: Confidentiality Read Application Data - Path traversal via unanchored validation may expose sensitive files. |
Example Code
Vulnerable Code
// Vulnerable: Missing anchors in path validation
$lang = $_GET['lang'];
// Vulnerable: Pattern matches if [A-Za-z0-9]+ appears ANYWHERE in input
if (preg_match("/[A-Za-z0-9]+/", $lang)) {
include("$dir/$lang"); // Path traversal possible!
}
// Attack: "../../etc/passwd" matches because "etc" and "passwd" are alphanumeric
// Result: include("$dir/../../etc/passwd") - reads /etc/passwd
# Vulnerable: IP validation without anchors
import re
import subprocess
ip_validator = re.compile(r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)")
def vulnerable_ping(ip):
# Vulnerable: Matches if valid IP pattern appears anywhere
if ip_validator.match(ip): # match() only anchors at start, not end
subprocess.call(["ping", "-c", "1", ip])
# Attack: "192.168.1.1; rm -rf /" matches because valid IP appears at start
# Attack: "0x7f.0.0.1" matches but represents 127.0.0.1 (hex notation)
// Vulnerable: Email validation without anchors
function vulnerableValidateEmail(email) {
// Vulnerable: Only checks if pattern appears somewhere
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
if (emailRegex.test(email)) {
return true; // Accepts invalid emails
}
return false;
}
// Attack: "malicious<script>@evil.com" passes
// Attack: "[email protected]<script>alert('xss')</script>" passes
// Vulnerable: Username validation without anchors
public boolean vulnerableValidateUsername(String username) {
// Vulnerable: Matches alphanumeric anywhere in string
Pattern pattern = Pattern.compile("[a-zA-Z0-9_]+");
Matcher matcher = pattern.matcher(username);
// find() matches anywhere, matches() would anchor
return matcher.find(); // Wrong method too!
}
// Attack: "admin'; DROP TABLE users;--" contains "admin" so it passes
# Vulnerable: URL validation without anchors
def vulnerable_validate_url(url)
# Vulnerable: Matches if pattern appears anywhere
if url =~ /https?:\/\/[a-zA-Z0-9.-]+/
return true
end
false
end
# Attack: "javascript:alert('xss')//http://valid.com" passes
# The regex matches "http://valid" but ignores leading javascript:
Fixed Code
// Fixed: Anchored path validation
$lang = $_GET['lang'];
// Fixed: ^ anchors start, $ anchors end - must match ENTIRE string
if (preg_match("/^[A-Za-z0-9]+$/", $lang)) {
include("$dir/$lang");
}
// "../etc/passwd" no longer matches - contains non-alphanumeric characters
// Only pure alphanumeric strings like "english" or "de" will match
# Fixed: Properly anchored IP validation
import re
import subprocess
# Fixed: ^ at start, $ at end
ip_validator = re.compile(r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
def fixed_ping(ip):
# Fixed: fullmatch() implicitly anchors, or use anchored pattern with match()
if ip_validator.fullmatch(ip):
subprocess.call(["ping", "-c", "1", ip])
# Alternative: Use search() with anchored pattern
def fixed_ping_v2(ip):
if ip_validator.search(ip) and ip_validator.match(ip).group() == ip:
subprocess.call(["ping", "-c", "1", ip])
# "192.168.1.1; rm -rf /" no longer matches
# Only valid IP addresses without extra content match
// Fixed: Anchored email validation
function fixedValidateEmail(email) {
// Fixed: ^ and $ anchor the pattern
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (emailRegex.test(email)) {
return true;
}
return false;
}
// "malicious<script>@evil.com" no longer passes - contains <
// "[email protected]<script>..." no longer passes - has extra content
// Fixed: Anchored username validation
public boolean fixedValidateUsername(String username) {
// Fixed: Anchors in pattern
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]+$");
Matcher matcher = pattern.matcher(username);
// matches() is better - implicitly anchors
return matcher.matches();
}
// Alternative: Use matches() which is implicitly anchored
public boolean fixedValidateUsernameV2(String username) {
return username.matches("[a-zA-Z0-9_]+"); // matches() anchors automatically
}
// "admin'; DROP TABLE users;--" no longer matches
# Fixed: Anchored URL validation
def fixed_validate_url(url)
# Fixed: \A anchors start, \z anchors end (Ruby-specific)
# Note: ^ and $ match line boundaries in Ruby, use \A and \z for string boundaries
if url =~ /\Ahttps?:\/\/[a-zA-Z0-9.-]+\z/
return true
end
false
end
# Alternative: More comprehensive URL validation
def fixed_validate_url_v2(url)
# Use strict anchoring
url.match?(/\Ahttps?:\/\/[a-zA-Z0-9.-]+(\/[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;=-]*)?\z/)
end
# "javascript:alert('xss')//http://valid.com" no longer passes
CVE Examples
- CVE-2022-30034: Python RPC framework's web UI used unanchored regex for validating user login emails, potentially enabling OAuth authentication bypass.
References
- MITRE Corporation. "CWE-777: Regular Expression without Anchors." https://cwe.mitre.org/data/definitions/777.html
- OWASP. "Input Validation Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- Regular-Expressions.info. "Anchors." https://www.regular-expressions.info/anchors.html