Regex Validation — Patterns, Injection, and Testing
In this tutorial, you'll learn about Regex Validation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Regex validation uses regular expressions to match string input against expected patterns, enabling precise format checks for emails, phone numbers, zip codes, and more.
What You'll Learn
By the end of this lesson, you will write regex patterns for common formats, prevent regex injection attacks, and test regex patterns systematically.
Why It Matters
Regex is the most powerful pattern-matching tool available, but it is also error-prone. A poorly written regex can reject valid data, accept invalid data, or even crash your server through catastrophic Backtracking.
Real-World Use
Durga Antivirus Pro uses regex patterns to match file signatures against known malware databases. A single incorrect regex can cause false positives that block legitimate software.
Regex Validation Flow
flowchart TD
Input[Raw Input] --> Normalize[Normalize/Trim]
Normalize --> Pattern[Match Against Pattern]
Pattern --> Match{Matches?}
Match -->|Yes| Accept[Accept Input]
Match -->|No| Reject[Return Error]
Reject --> Escape{Contains Regex Metacharacters?}
Escape -->|Yes| Sanitize[Log and Sanitize]
Common Pattern Validators
# common_patterns.py
import re
from typing import Optional
class PatternValidators:
@staticmethod
def email(value: str) -> Optional[str]:
if not isinstance(value, str):
return "Email must be a string"
# RFC 5322 simplified pattern
pattern = r'^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$'
if not re.match(pattern, value.strip()):
return "Invalid email format"
return None
@staticmethod
def phone_us(value: str) -> Optional[str]:
if not isinstance(value, str):
return "Phone must be a string"
cleaned = re.sub(r'[\s\-\(\)\.]', '', value)
pattern = r'^\+?1?\d{10}$'
if not re.match(pattern, cleaned):
return "Invalid US phone (expected 10 digits)"
return None
@staticmethod
def zip_us(value: str) -> Optional[str]:
if not isinstance(value, str):
return "ZIP must be a string"
pattern = r'^\d{5}(-\d{4})?$'
if not re.match(pattern, value.strip()):
return "Invalid ZIP code (format: 12345 or 12345-6789)"
return None
@staticmethod
def password_strength(value: str) -> Optional[str]:
if not isinstance(value, str):
return "Password must be a string"
if len(value) < 8:
return "Minimum 8 characters"
if not re.search(r'[A-Z]', value):
return "Must contain uppercase letter"
if not re.search(r'[a-z]', value):
return "Must contain lowercase letter"
if not re.search(r'[0-9]', value):
return "Must contain digit"
if not re.search(r'[!@#$%^&*(),.?\":{}|<>]', value):
return "Must contain special character"
return None
@staticmethod
def uuid(value: str) -> Optional[str]:
if not isinstance(value, str):
return "UUID must be a string"
pattern = r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
if not re.match(pattern, value.strip()):
return "Invalid UUID format (xXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)"
return None
pv = PatternValidators()
tests = [
("user@example.com", pv.email),
("invalid@", pv.email),
("+14155550100", pv.phone_us),
("12345", pv.phone_us),
("90210", pv.zip_us),
("9021", pv.zip_us),
("Abc123!@", pv.password_strength),
("weak", pv.password_strength),
("550e8400-e29b-41d4-a716-446655440000", pv.uuid),
("not-a-uuid", pv.uuid),
]
for value, validator in tests:
error = validator(value)
status = "VALID" if not error else error
print(f" {str(value):45s} -> {status}")
Expected output:
user@example.com -> VALID
invalid@ -> Invalid email format
+14155550100 -> VALID
12345 -> Invalid US phone (expected 10 digits)
90210 -> VALID
9021 -> Invalid ZIP code (format: 12345 or 12345-6789)
Abc123!@ -> VALID
weak -> Minimum 8 characters
550e8400-e29b-41d4-a716-446655440000 -> VALID
not-a-uuid -> Invalid UUID format
Regex Injection Prevention
# regex_injection_prevention.py
import re
from typing import Optional
class RegexInjectionPrevention:
@staticmethod
def escape_user_input(pattern: str) -> str:
escaped = re.escape(pattern)
return escaped
@staticmethod
def safe_search(pattern: str, text: str) -> Optional[str]:
try:
if re.search(pattern, text):
return "Match found"
return "No match"
except re.error as e:
return f"Regex error: {e}"
@staticmethod
def validate_against_user_pattern(user_pattern: str, text: str) -> str:
escaped_pattern = re.escape(user_pattern)
safe_pattern = f".*{escaped_pattern}.*"
return RegexInjectionPrevention.safe_search(safe_pattern, text)
untrusted = ".*\n.*(.*)"
safe_pattern = re.escape(untrusted)
print(f"User input: {untrusted}")
print(f"Escaped: {safe_pattern}")
# Dangerous: using raw user input as regex
dangerous = RegexInjectionPrevention.safe_search(f".*{untrusted}.*", "test")
print(f"Dangerous match: {dangerous}")
# Safe: using escaped pattern
safe_result = RegexInjectionPrevention.safe_search(f".*{safe_pattern}.*", "test")
print(f"Safe match: {safe_result}")
# ReDoS protection: limit pattern complexity
def safe_user_search(user_pattern: str, text: str, max_length: int = 100) -> str:
if len(user_pattern) > max_length:
return "Pattern too long"
return RegexInjectionPrevention.validate_against_user_pattern(user_pattern, text)
print(f"Long pattern: {safe_user_search('a' * 200, 'test')}")
Expected output:
User input: .*
(.*)
Escaped: \.\*\n\(\.\*\)
Dangerous match: Match found
Safe match: No match
Long pattern: Pattern too long
Regex Testing Framework
# regex_tester.py
import re
from typing import Any, Dict, List, Tuple
class RegexTester:
def __init__(self, pattern: str):
try:
self.compiled = re.compile(pattern)
self.valid = True
except re.error as e:
self.compiled = None
self.valid = False
self.error = str(e)
def test(self, cases: List[Tuple[str, bool]]) -> Dict[str, Any]:
results = {"passed": [], "failed": [], "error": None}
if not self.valid:
results["error"] = self.error
return results
for value, expected in cases:
actual = bool(self.compiled.match(value))
entry = {"value": value, "expected": expected, "actual": actual}
if actual == expected:
results["passed"].append(entry)
else:
results["failed"].append(entry)
return results
def catastrophic_backtracking_risk(self) -> bool:
# Detect nested quantifiers that cause catastrophic backtracking
if not self.valid:
return False
pattern = self.compiled.pattern
risk_patterns = [r'\([^)]*\)\*\+\?', r'\[\^?\]\*\+', r'\(\.\*\)\{']
for risk in risk_patterns:
if re.search(risk, pattern):
return True
return False
tester = RegexTester(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
cases = [
("user@example.com", True),
("missing-at", False),
("user@.com", False),
("user@domain.c", False),
]
result = tester.test(cases)
print(f"Passed: {len(result['passed'])}")
print(f"Failed: {len(result['failed'])}")
for f in result["failed"]:
print(f" FAIL: {f['value']} expected={f['expected']} actual={f['actual']}")
risk_tester = RegexTester(r'^(a|aa)+$')
print(f"Catastrophic backtracking risk: {risk_tester.catastrophic_backtracking_risk()}")
safe_tester = RegexTester(r'^[a-z]{3,10}$')
print(f"Safe pattern risk: {safe_tester.catastrophic_backtracking_risk()}")
Expected output:
Passed: 3
Failed: 1
FAIL: user@domain.c expected=False actual=True
Catastrophic backtracking risk: True
Safe pattern risk: False
Pattern Builder
# pattern_builder.py
from typing import Dict, List, Optional
class PatternBuilder:
def __init__(self):
self.parts: List[str] = []
self.anchors: bool = True
self.case_sensitive: bool = True
def start(self) -> 'PatternBuilder':
self.parts = []
return self
def literal(self, text: str) -> 'PatternBuilder':
import re
self.parts.append(re.escape(text))
return self
def digit(self, count: int = 1) -> 'PatternBuilder':
self.parts.append(f'\\d{{{count}}}')
return self
def letter(self, count: int = 1) -> 'PatternBuilder':
self.parts.append(f'[a-zA-Z]{{{count}}}')
return self
def optional(self, pattern: str) -> 'PatternBuilder':
self.parts.append(f'({pattern})?')
return self
def group(self, pattern: str) -> 'PatternBuilder':
self.parts.append(f'({pattern})')
return self
def any_of(self, chars: str) -> 'PatternBuilder':
safe_chars = chars.replace('[', '').replace(']', '')
self.parts.append(f'[{safe_chars}]')
return self
def build(self) -> str:
pattern = ''.join(self.parts)
if self.anchors:
pattern = '^' + pattern + '$'
return pattern
def separator(self, char: str = '-') -> 'PatternBuilder':
self.parts.append(re.escape(char))
return self
builder = PatternBuilder()
# Build a US phone pattern: ^\d{3}-\d{3}-\d{4}$
phone_pattern = (builder.start()
.digit(3).separator().digit(3).separator().digit(4)
).build()
print(f"Phone pattern: {phone_pattern}")
# Build a date pattern: ^\d{4}-\d{2}-\d{2}$
date_pattern = (PatternBuilder().start()
.digit(4).literal('-').digit(2).literal('-').digit(2)
).build()
print(f"Date pattern: {date_pattern}")
# Build a product code: ^[A-Z]{2}-\d{4}$
code_pattern = (PatternBuilder().start()
.any_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ").digit(4)
).build()
print(f"Product code: {code_pattern}")
Expected output:
Phone pattern: ^\d{3}-\d{3}-\d{4}$
Date pattern: ^\d{4}-\d{2}-\d{2}$
Product code: ^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]\d{4}$
Common Mistakes
1. Missing Anchors
A pattern [a-z]+ matches anywhere in the string. Always use ^...$ for full-string validation.
2. Catastrophic Backtracking
Patterns like (a|aa)+$ or (.*)* cause exponential backtracking on near-matches. Use atomic groups or possessive quantifiers.
3. Not Escaping User Input
Passing untrusted input directly to re.compile() or re.match() allows regex injection. Always escape user-supplied patterns.
4. Overly Complex Patterns
A 200-character regex is harder to test, maintain, and understand. Break complex patterns into smaller validators.
5. Case Sensitivity Mismatch
Forgetting to set re.IGNORECASE when validating emails or URLs can cause false rejections.
Practice Questions
1. What does the ^ and $ anchor do in regex?
^ matches the start of the string, $ matches the end. Together they ensure the entire string matches.
2. What is catastrophic backtracking?
A regex engine trying exponentially many ways to match nested quantifiers, potentially freezing or crashing.
3. How do you prevent regex injection?
Escape user input with re.escape() before incorporating it into a pattern. Never pass raw input to re.compile().
4. What is the difference between re.match() and re.search()?
re.match() checks only the beginning of the string. re.search() checks anywhere. Use match with anchors for full validation.
Challenge
Build a pattern library that validates: email, US phone, international phone (E.164), ZIP+4, MAC address, IPv4, IPv6, UUID, base64, and hex color. Include a test suite with edge cases.
FAQ
Mini Project: Regex Validator Library
# regex_validator_lib.py
import re
from typing import Callable, Dict, List, Optional, Tuple
class RegexValidator:
def __init__(self):
self.patterns: Dict[str, Tuple[str, Optional[str]]] = {}
def add(self, name: str, pattern: str, flags: int = 0, error_msg: Optional[str] = None):
try:
re.compile(pattern, flags)
except re.error as e:
raise ValueError(f"Invalid regex for {name}: {e}")
self.patterns[name] = (pattern, error_msg or f"Invalid {name}")
def validate(self, name: str, value: str) -> Optional[str]:
entry = self.patterns.get(name)
if not entry:
return f"Unknown pattern: {name}"
pattern, message = entry
if not re.match(pattern, value.strip()):
return message
return None
def validate_all(self, rules: Dict[str, str]) -> Dict[str, str]:
errors = {}
for name, value in rules.items():
error = self.validate(name, value)
if error:
errors[name] = error
return errors
rv = RegexValidator()
rv.add("email", r'^[^@]+@[^@]+\.[^@]+$', error_msg="Invalid email format")
rv.add("zip_us", r'^\d{5}(-\d{4})?$', error_msg="Invalid ZIP code")
rv.add("hex_color", r'^#[0-9a-fA-F]{6}$')
print(rv.validate("email", "user@example.com"))
print(rv.validate("email", "bad"))
print(rv.validate_all({"email": "user@example.com", "zip_us": "90210"}))
print(rv.validate_all({"email": "bad", "zip_us": "902"}))
Expected output:
None
Invalid email format
{}
{'email': 'Invalid email format', 'zip_us': 'Invalid ZIP code'}
What's Next
You understand regex validation. Next, learn cross-field validation, then custom validation rules.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro