182 lines
4.5 KiB
TypeScript
182 lines
4.5 KiB
TypeScript
/**
|
|
* Regex Validation Utilities.
|
|
*
|
|
* Provides security validation for user-supplied regex patterns
|
|
* to prevent ReDoS (Regular Expression Denial of Service) attacks.
|
|
*/
|
|
|
|
/**
|
|
* Maximum allowed length for a regex pattern.
|
|
*/
|
|
const MAX_PATTERN_LENGTH = 100;
|
|
|
|
/**
|
|
* Patterns that indicate potentially problematic regex constructs.
|
|
* These can cause exponential backtracking (ReDoS).
|
|
*/
|
|
const DANGEROUS_PATTERNS: RegExp[] = [
|
|
// Nested quantifiers: (a+)+, (a*)+, (a+)*, (a*)*
|
|
/\([^)]{0,50}[+*][^)]{0,50}\)[+*]/,
|
|
// Overlapping alternation with quantifiers: (a|a)+
|
|
/\([^)|]{0,50}\|[^)]{0,50}\)[+*]/,
|
|
// Multiple quantifiers on same group: a{1,}+
|
|
/[+*]\{/,
|
|
/\}[+*]/,
|
|
// Backreferences with quantifiers (can cause exponential time)
|
|
/\\[1-9][+*]/,
|
|
// Very long character classes with quantifiers
|
|
/\[[^\]]{20}\][+*]/,
|
|
];
|
|
|
|
/**
|
|
* Characters that need to be balanced in a valid regex.
|
|
*/
|
|
const BALANCED_PAIRS: [string, string][] = [
|
|
['(', ')'],
|
|
['[', ']'],
|
|
['{', '}'],
|
|
];
|
|
|
|
/**
|
|
* Result of regex pattern validation.
|
|
*/
|
|
export interface RegexValidationResult {
|
|
valid: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Checks if brackets in a string are balanced.
|
|
*/
|
|
function areBracketsBalanced(pattern: string): boolean {
|
|
const stack: string[] = [];
|
|
const openBrackets = new Map(BALANCED_PAIRS.map(([open, close]) => [open, close]));
|
|
const closeBrackets = new Map(BALANCED_PAIRS.map(([open, close]) => [close, open]));
|
|
|
|
let escaped = false;
|
|
let inCharClass = false;
|
|
|
|
for (const char of pattern) {
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
|
|
if (char === '\\') {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
|
|
// Track character class state
|
|
if (char === '[' && !inCharClass) {
|
|
inCharClass = true;
|
|
stack.push(char);
|
|
continue;
|
|
}
|
|
|
|
if (char === ']' && inCharClass) {
|
|
inCharClass = false;
|
|
if (stack.length === 0 || stack[stack.length - 1] !== '[') {
|
|
return false;
|
|
}
|
|
stack.pop();
|
|
continue;
|
|
}
|
|
|
|
// Skip bracket matching inside character classes
|
|
if (inCharClass) {
|
|
continue;
|
|
}
|
|
|
|
if (openBrackets.has(char)) {
|
|
stack.push(char);
|
|
} else if (closeBrackets.has(char)) {
|
|
const expectedOpen = closeBrackets.get(char);
|
|
if (stack.length === 0 || stack[stack.length - 1] !== expectedOpen) {
|
|
return false;
|
|
}
|
|
stack.pop();
|
|
}
|
|
}
|
|
|
|
return stack.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Validates a regex pattern for safety and correctness.
|
|
*
|
|
* Security checks performed:
|
|
* 1. Length limit (max 100 chars)
|
|
* 2. Dangerous pattern detection (nested quantifiers, etc.)
|
|
* 3. Balanced brackets
|
|
* 4. Valid regex syntax (via RegExp constructor)
|
|
*
|
|
* @param pattern - The regex pattern to validate
|
|
* @returns Validation result with error message if invalid
|
|
*/
|
|
export function validateRegexPattern(pattern: string): RegexValidationResult {
|
|
// Empty pattern check
|
|
if (!pattern || typeof pattern !== 'string') {
|
|
return { valid: false, error: 'Pattern must be a non-empty string' };
|
|
}
|
|
|
|
// Length check
|
|
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
return {
|
|
valid: false,
|
|
error: `Pattern too long (max ${MAX_PATTERN_LENGTH} characters)`,
|
|
};
|
|
}
|
|
|
|
// Check for dangerous patterns that could cause ReDoS
|
|
for (const dangerous of DANGEROUS_PATTERNS) {
|
|
if (dangerous.test(pattern)) {
|
|
return {
|
|
valid: false,
|
|
error: 'Pattern contains constructs that could cause performance issues',
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check bracket balance
|
|
if (!areBracketsBalanced(pattern)) {
|
|
return {
|
|
valid: false,
|
|
error: 'Pattern has unbalanced brackets',
|
|
};
|
|
}
|
|
|
|
// Try to compile the regex to check for syntax errors
|
|
try {
|
|
new RegExp(pattern);
|
|
} catch (e) {
|
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
|
return {
|
|
valid: false,
|
|
error: `Invalid regex syntax: ${message}`,
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Creates a safe RegExp from a pattern, returning null if invalid.
|
|
* This is a convenience wrapper that validates and creates the regex.
|
|
*
|
|
* @param pattern - The regex pattern
|
|
* @param flags - Optional regex flags (default: 'i' for case-insensitive)
|
|
* @returns The compiled RegExp or null if validation fails
|
|
*/
|
|
export function createSafeRegExp(pattern: string, flags: string = 'i'): RegExp | null {
|
|
const validation = validateRegexPattern(pattern);
|
|
if (!validation.valid) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return new RegExp(pattern, flags);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|