agent-ecosystem/src/main/utils/regexValidation.ts

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