Add ReDoS validation for SpamRule.pattern field (FRE-4512)
- Create regex-validation utility with ReDoS detection (nested quantifiers, overlapping alternations, complexity limits) - Add @db.VarChar(500) constraint on pattern field in Prisma schema - Integrate validation in rule-engine at load time and evaluation time - Add 46 unit tests covering syntax, ReDoS patterns, complexity, edge cases Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { PrismaClient, SpamRule } from '@prisma/client';
|
||||
import { generateRequestId } from '@shieldai/types';
|
||||
import { validateRegexPattern, RegexValidationError } from '../utils/regex-validation';
|
||||
|
||||
export interface RuleMatch {
|
||||
ruleId: string;
|
||||
@@ -38,7 +39,7 @@ export class RuleEngine {
|
||||
|
||||
async loadActiveRules(): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
|
||||
if (this.config.enableCache && this.lastLoadTime) {
|
||||
const elapsed = now.getTime() - this.lastLoadTime.getTime();
|
||||
if (elapsed < this.config.loadIntervalMs) {
|
||||
@@ -51,10 +52,24 @@ export class RuleEngine {
|
||||
orderBy: { priority: 'desc' },
|
||||
});
|
||||
|
||||
this.allRules = rules;
|
||||
this.numberPatternRules = rules.filter(r => r.category === 'number_pattern');
|
||||
this.behavioralRules = rules.filter(r => r.category === 'behavioral');
|
||||
this.contentRules = rules.filter(r => r.category === 'content');
|
||||
const validatedRules: SpamRule[] = [];
|
||||
for (const rule of rules) {
|
||||
try {
|
||||
validateRegexPattern(rule.pattern);
|
||||
validatedRules.push(rule);
|
||||
} catch (error) {
|
||||
if (error instanceof RegexValidationError) {
|
||||
console.warn(`[RuleEngine] [req:${generateRequestId()}] Rule "${rule.name}" (${rule.id}) ReDoS risk: ${error.reason}, skipping`);
|
||||
} else {
|
||||
console.error(`[RuleEngine] [req:${generateRequestId()}] Unexpected error validating rule "${rule.name}" (${rule.id}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.allRules = validatedRules;
|
||||
this.numberPatternRules = validatedRules.filter(r => (r as any).category === 'number_pattern');
|
||||
this.behavioralRules = validatedRules.filter(r => (r as any).category === 'behavioral');
|
||||
this.contentRules = validatedRules.filter(r => (r as any).category === 'content');
|
||||
this.lastLoadTime = now;
|
||||
}
|
||||
|
||||
@@ -67,19 +82,24 @@ export class RuleEngine {
|
||||
|
||||
for (const rule of this.allRules) {
|
||||
try {
|
||||
validateRegexPattern(rule.pattern);
|
||||
const pattern = new RegExp(rule.pattern);
|
||||
if (pattern.test(phoneNumber)) {
|
||||
matches.push({
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
pattern: rule.pattern,
|
||||
score: rule.score,
|
||||
priority: rule.priority as 'high' | 'medium' | 'low',
|
||||
score: (rule as any).score,
|
||||
priority: (rule as any).priority as 'high' | 'medium' | 'low',
|
||||
matchedAt: new Date(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[RuleEngine] [req:${generateRequestId()}] Invalid pattern for rule ${rule.id}:`, error);
|
||||
if (error instanceof RegexValidationError) {
|
||||
console.warn(`[RuleEngine] [req:${generateRequestId()}] Rule "${rule.name}" (${rule.id}) ReDoS risk at eval: ${error.reason}`);
|
||||
} else {
|
||||
console.error(`[RuleEngine] [req:${generateRequestId()}] Invalid pattern for rule ${rule.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,19 +115,24 @@ export class RuleEngine {
|
||||
|
||||
for (const rule of this.contentRules) {
|
||||
try {
|
||||
validateRegexPattern(rule.pattern);
|
||||
const pattern = new RegExp(rule.pattern, 'i');
|
||||
if (pattern.test(smsBody)) {
|
||||
matches.push({
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
pattern: rule.pattern,
|
||||
score: rule.score,
|
||||
priority: rule.priority as 'high' | 'medium' | 'low',
|
||||
score: (rule as any).score,
|
||||
priority: (rule as any).priority as 'high' | 'medium' | 'low',
|
||||
matchedAt: new Date(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[RuleEngine] [req:${generateRequestId()}] Invalid pattern for rule ${rule.id}:`, error);
|
||||
if (error instanceof RegexValidationError) {
|
||||
console.warn(`[RuleEngine] [req:${generateRequestId()}] Rule "${rule.name}" (${rule.id}) ReDoS risk at eval: ${error.reason}`);
|
||||
} else {
|
||||
console.error(`[RuleEngine] [req:${generateRequestId()}] Invalid pattern for rule ${rule.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user