Permissive Regular Expression

Description

Permissive Regular Expression occurs when a regex pattern intended for input validation is too lenient, allowing inputs that should be rejected. Common issues include anchoring problems (not using ^ and $), greedy quantifiers that match too much, character class errors, and failing to account for special characters or encodings. The regex appears to validate but actually passes malicious input.

Risk

Input validation bypasses allow injection attacks (SQL, XSS, command). Malformed data enters the system causing errors or unexpected behavior. Security filters that rely on regex can be circumvented. Data integrity issues arise from improperly validated input. Authentication may be bypassed if username/password validation is too permissive.

Solution

Use anchors (^ and $) to match entire string. Test regex against known-bad inputs. Use non-greedy quantifiers where appropriate. Consider using dedicated validators instead of custom regex. Test edge cases including empty strings, special characters, and different encodings. Review regex for common mistakes. Use regex testing tools to verify behavior.

Common Consequences

ImpactDetails
SecurityScope: Validation Bypass

Malicious input passes validation checks.
IntegrityScope: Data Corruption

Invalid data enters the system.
AuthenticationScope: Bypass

Invalid credentials may be accepted.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Missing anchors
function validateEmailVulnerable(email) {
    // Missing ^ and $ - matches partial string
    return /\w+@\w+\.\w+/.test(email);
}

// Passes validation:
validateEmailVulnerable("evil<script>@test.com");  // true! (matches partial)
validateEmailVulnerable("[email protected]<script>"); // true!

// VULNERABLE: Overly permissive character class
function validateUsernameVulnerable(username) {
    // Allows too many characters
    return /^.{3,20}$/.test(username);
}

// Passes:
validateUsernameVulnerable("admin'--");  // SQL injection chars pass
validateUsernameVulnerable("<script>");  // XSS chars pass

// VULNERABLE: Wrong character class
function validatePhoneVulnerable(phone) {
    // [0-9] intended but wrong syntax
    return /^[0-9-]+$/.test(phone);
}

// The - creates a range, passes unexpected:
validatePhoneVulnerable("---");  // true
validatePhoneVulnerable("9-0");  // true

// VULNERABLE: Greedy quantifier issues
function extractURLVulnerable(text) {
    // .* is greedy, matches too much
    const match = text.match(/href="(.*)"/);
    return match ? match[1] : null;
}

// Given: href="http://good.com" onclick="evil()"
// Returns: http://good.com" onclick="evil()
// Because .* greedily matches to last "

// VULNERABLE: Case sensitivity
function validateCommandVulnerable(cmd) {
    // Missing case-insensitive flag
    return /^(GET|POST|PUT|DELETE)$/.test(cmd);
}

// Passes unexpected:
validateCommandVulnerable("get");   // false - but should maybe be valid
validateCommandVulnerable("Get");   // false

// VULNERABLE: Unicode/encoding issues
function validateNameVulnerable(name) {
    // \w doesn't match unicode letters
    return /^\w+$/.test(name);
}

// Rejects valid names:
validateNameVulnerable("José");    // false
validateNameVulnerable("北京");    // false
# VULNERABLE: Partial matching
import re

def validate_ip_vulnerable(ip):
    # Missing ^ and $ - partial match
    pattern = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
    return re.search(pattern, ip) is not None

# Passes:
validate_ip_vulnerable("evil192.168.1.1evil")  # True!
validate_ip_vulnerable("999.999.999.999")       # True (invalid IP)

# VULNERABLE: Escape sequence issues
def validate_path_vulnerable(path):
    # Dot not escaped - matches any char
    pattern = r'^/var/www/.+.html$'
    return re.match(pattern, path) is not None

# Passes unexpected:
validate_path_vulnerable("/var/www/xhtml")      # True! (dot matches x)
validate_path_vulnerable("/var/www/../etc/passwd.html")  # True!

# VULNERABLE: Multiline issues
def validate_header_vulnerable(header):
    # Doesn't handle newlines properly
    pattern = r'^[A-Za-z-]+: .+$'
    return re.match(pattern, header) is not None

# Header injection:
validate_header_vulnerable("Header: value\r\nEvil: injected")
# Matches only first line, ignores injection
// VULNERABLE: Pattern compilation issues
public class VulnerableValidator {

    // Missing CASE_INSENSITIVE might be intentional but often isn't
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("\\w+@\\w+\\.\\w+");  // No anchors!

    public boolean validateEmail(String email) {
        return EMAIL_PATTERN.matcher(email).find();  // find() not matches()!
    }

    // VULNERABLE: DOTALL mode issues
    private static final Pattern SCRIPT_PATTERN =
        Pattern.compile("<script>.*</script>", Pattern.DOTALL);

    public String removeScripts(String html) {
        // Greedy .* with DOTALL removes too much
        return SCRIPT_PATTERN.matcher(html).replaceAll("");
    }
}

Fixed Code

// SAFE: Proper anchors and character classes
function validateEmailSafe(email) {
    // Anchors ensure full string match
    // More precise character classes
    const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return pattern.test(email);
}

// SAFE: Restrictive username validation
function validateUsernameSafe(username) {
    // Only allow specific safe characters
    // Anchors ensure complete match
    return /^[a-zA-Z0-9_-]{3,20}$/.test(username);
}

// SAFE: Proper phone validation
function validatePhoneSafe(phone) {
    // Escape hyphen or put at end of class
    return /^[0-9\-]+$/.test(phone);

    // Or more specific format:
    // return /^\d{3}-\d{3}-\d{4}$/.test(phone);
}

// SAFE: Non-greedy quantifier
function extractURLSafe(text) {
    // .*? is non-greedy, matches minimum
    const match = text.match(/href="(.*?)"/);
    return match ? match[1] : null;

    // Even better - exclude quote from class:
    // const match = text.match(/href="([^"]*)"/);
}

// SAFE: Case handling
function validateCommandSafe(cmd) {
    // Case-insensitive flag
    return /^(GET|POST|PUT|DELETE)$/i.test(cmd);
}

// SAFE: Unicode support
function validateNameSafe(name) {
    // Unicode letter category
    return /^[\p{L}\p{M}' -]+$/u.test(name);
}

// SAFE: Input length limits
function validateInputSafe(input, maxLength = 100) {
    if (input.length > maxLength) {
        return false;
    }
    return /^[a-zA-Z0-9_-]+$/.test(input);
}
import re
import ipaddress

# SAFE: Full string matching with anchors
def validate_ip_safe(ip):
    # Use anchors
    pattern = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
    if not re.match(pattern, ip):
        return False

    # Additional validation for actual IP validity
    try:
        ipaddress.ip_address(ip)
        return True
    except ValueError:
        return False

# SAFE: Proper escaping
def validate_path_safe(path):
    # Escape special regex characters
    pattern = r'^/var/www/[a-zA-Z0-9_-]+\.html$'
    if not re.match(pattern, path):
        return False

    # Check for path traversal
    if '..' in path:
        return False

    return True

# SAFE: Multiline handling
def validate_header_safe(header):
    # Reject if contains newlines
    if '\r' in header or '\n' in header:
        return False

    pattern = r'^[A-Za-z-]+: .+$'
    return re.match(pattern, header) is not None

# SAFE: Use fullmatch (Python 3.4+)
def validate_username_safe(username):
    # fullmatch requires complete string match
    pattern = r'[a-zA-Z0-9_]{3,20}'
    return re.fullmatch(pattern, username) is not None

# SAFE: Comprehensive email validation
def validate_email_safe(email):
    # Length check first
    if len(email) > 254:
        return False

    # RFC 5322 compliant pattern (simplified)
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

    if not re.fullmatch(pattern, email):
        return False

    # Additional checks
    local, domain = email.rsplit('@', 1)
    if len(local) > 64:
        return False

    return True
// SAFE: Proper Java regex usage
public class SafeValidator {

    // Anchored pattern
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");

    public boolean validateEmail(String email) {
        // Use matches() which requires full match
        return EMAIL_PATTERN.matcher(email).matches();
    }

    // SAFE: Non-greedy script removal
    private static final Pattern SCRIPT_PATTERN =
        Pattern.compile("<script[^>]*>.*?</script>",
                        Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    public String removeScripts(String html) {
        // Non-greedy .*? matches each script tag individually
        return SCRIPT_PATTERN.matcher(html).replaceAll("");
    }

    // SAFE: Input validation with bounds
    public boolean validateUsername(String username) {
        if (username == null || username.length() > 50) {
            return false;
        }

        return username.matches("^[a-zA-Z0-9_]{3,20}$");
    }
}

Exploited in the Wild

Email Validation Bypass

Permissive email regex allowed XSS payloads in "email" fields.

URL Filter Bypass

Overly permissive URL validation allowed malicious redirects.

SQL Injection

Input validation regex bypassed, allowing SQL injection.


Tools to test/exploit

  • regex101 — test and debug regex.

  • RegexBuddy — regex development tool.

  • Fuzzing tools with malicious input patterns.


CVE Examples

  • CVEs from regex validation bypass in web applications.

  • WAF bypass through permissive regex rules.


References

  1. MITRE. "CWE-625: Permissive Regular Expression." https://cwe.mitre.org/data/definitions/625.html

  2. OWASP. "Input Validation Cheat Sheet."