/** * 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; }}
/**
* 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;
}
}