sicua
Version:
A tool for analyzing project structure and dependencies
330 lines (329 loc) • 13.7 kB
JavaScript
"use strict";
/**
* Detector for Regular Expression Denial of Service (ReDoS) vulnerabilities
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedosPatternDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const ASTTraverser_1 = require("../utils/ASTTraverser");
class RedosPatternDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("RedosPatternDetector", "redos-vulnerability", "high", RedosPatternDetector.REDOS_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter relevant files
const relevantFiles = this.filterRelevantFiles(scanResult, [".ts", ".tsx", ".js", ".jsx"], [
"node_modules",
"dist",
"build",
".git",
"coverage",
"__tests__",
".test.",
".spec.",
]);
for (const filePath of relevantFiles) {
const content = scanResult.fileContents.get(filePath);
if (!content)
continue;
// Only use AST-based analysis for accurate regex detection
const sourceFile = scanResult.sourceFiles.get(filePath);
if (sourceFile) {
const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForRedosPatterns(sf, fp));
// Adjust confidence based on file context and validate
const fileContext = this.getFileContext(filePath, content);
for (const vuln of astVulnerabilities) {
vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext);
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
}
return vulnerabilities;
}
/**
* AST-based analysis for ReDoS patterns
*/
analyzeASTForRedosPatterns(sourceFile, filePath) {
const vulnerabilities = [];
// Find regex literals (/pattern/flags)
const regexLiterals = this.findRegexLiterals(sourceFile);
for (const regexLiteral of regexLiterals) {
const redosVuln = this.analyzeRegexLiteral(regexLiteral, sourceFile, filePath);
if (redosVuln) {
vulnerabilities.push(redosVuln);
}
}
// Find RegExp constructor calls (new RegExp("pattern"))
const regexpConstructors = this.findRegExpConstructors(sourceFile);
for (const regexpConstructor of regexpConstructors) {
const redosVuln = this.analyzeRegExpConstructor(regexpConstructor, sourceFile, filePath);
if (redosVuln) {
vulnerabilities.push(redosVuln);
}
}
// Find string.match(), string.replace() with regex patterns
const regexMethodCalls = this.findRegexMethodCalls(sourceFile);
for (const methodCall of regexMethodCalls) {
const methodVuln = this.analyzeRegexMethodCall(methodCall, sourceFile, filePath);
if (methodVuln) {
vulnerabilities.push(methodVuln);
}
}
return vulnerabilities;
}
/**
* Find method calls that use regex patterns
*/
findRegexMethodCalls(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => {
if (typescript_1.default.isPropertyAccessExpression(node.expression)) {
const method = node.expression.name;
if (typescript_1.default.isIdentifier(method)) {
const methodName = method.text;
return [
"match",
"replace",
"test",
"exec",
"search",
"split",
].includes(methodName);
}
}
return false;
});
}
/**
* Analyze regex method calls
*/
analyzeRegexMethodCall(methodCall, sourceFile, filePath) {
if (methodCall.arguments.length === 0)
return null;
const firstArg = methodCall.arguments[0];
// Only analyze if the first argument is a regex literal
if (typescript_1.default.isRegularExpressionLiteral(firstArg)) {
return this.analyzeRegexLiteral(firstArg, sourceFile, filePath);
}
return null;
}
/**
* Improved regex analysis with better pattern detection
*/
analyzeRegexForReDoS(pattern) {
// More precise dangerous pattern detection
const exponentialPatterns = [
/\([^)]*[+*][^)]*\)[+*]/, // (a+)+ or (a*)*
/\([^)]*[+*][^)]*\)\{[0-9,]+\}/, // (a+){2,}
/\([^)]*\|[^)]*\)[+*]/, // (a|b)+
];
const nestedQuantifierPatterns = [
/\([^)]*\([^)]*[+*][^)]*\)[^)]*\)[+*]/, // ((a+)b)+
/\([^)]*[+*][^)]*\)[+*]/, // (a+)+
];
// Check for exponential backtracking
for (const expPattern of exponentialPatterns) {
if (expPattern.test(pattern)) {
return {
description: `Regular expression contains exponential backtracking pattern: ${pattern}`,
severity: "high",
confidence: "high",
type: "exponential-backtracking",
complexityEstimate: "O(2^n)",
recommendations: [
"Rewrite regex to avoid nested quantifiers",
"Use atomic groups or possessive quantifiers",
"Add input length limits",
"Consider using a parsing library instead",
],
};
}
}
// Check for nested quantifiers
for (const nestedPattern of nestedQuantifierPatterns) {
if (nestedPattern.test(pattern)) {
return {
description: `Regular expression contains nested quantifiers: ${pattern}`,
severity: "medium",
confidence: "high",
type: "nested-quantifiers",
complexityEstimate: "O(n^k)",
recommendations: [
"Simplify nested quantifier structure",
"Test with long input strings",
"Consider breaking into multiple simpler patterns",
],
};
}
}
return null;
}
/**
* Find regex literal expressions
*/
findRegexLiterals(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.RegularExpressionLiteral);
}
/**
* Find RegExp constructor calls
*/
findRegExpConstructors(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.NewExpression, (node) => {
return (typescript_1.default.isIdentifier(node.expression) && node.expression.text === "RegExp");
});
}
/**
* Analyze regex literal for ReDoS patterns
*/
analyzeRegexLiteral(regexLiteral, sourceFile, filePath) {
const regexText = regexLiteral.text;
const regexPattern = this.extractRegexPattern(regexText);
if (!regexPattern)
return null;
const redosAnalysis = this.analyzeRegexForReDoS(regexPattern);
if (!redosAnalysis)
return null;
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(regexLiteral, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(regexLiteral, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(regexLiteral, sourceFile);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(regexLiteral),
}, redosAnalysis.description, redosAnalysis.severity, redosAnalysis.confidence, {
regexPattern,
vulnerabilityType: redosAnalysis.type,
complexityEstimate: redosAnalysis.complexityEstimate,
recommendations: redosAnalysis.recommendations,
detectionMethod: "regex-literal-analysis",
});
}
/**
* Analyze RegExp constructor for ReDoS patterns
*/
analyzeRegExpConstructor(regexpConstructor, sourceFile, filePath) {
if (!regexpConstructor.arguments ||
regexpConstructor.arguments.length === 0) {
return null;
}
const firstArg = regexpConstructor.arguments[0];
if (!typescript_1.default.isStringLiteral(firstArg)) {
return null; // Can't analyze dynamic patterns
}
const regexPattern = firstArg.text;
const redosAnalysis = this.analyzeRegexForReDoS(regexPattern);
if (!redosAnalysis)
return null;
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(regexpConstructor, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(regexpConstructor, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(regexpConstructor, sourceFile);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(regexpConstructor),
}, redosAnalysis.description, redosAnalysis.severity, redosAnalysis.confidence, {
regexPattern,
vulnerabilityType: redosAnalysis.type,
complexityEstimate: redosAnalysis.complexityEstimate,
recommendations: redosAnalysis.recommendations,
detectionMethod: "regexp-constructor-analysis",
});
}
/**
* Extract regex pattern from literal text
*/
extractRegexPattern(regexText) {
// Remove the surrounding / and flags
const match = regexText.match(/^\/(.*)\/[gimuy]*$/);
return match ? match[1] : null;
}
/**
* Extract function name from AST node context
*/
extractFunctionFromAST(node) {
let current = node.parent;
while (current) {
if (typescript_1.default.isFunctionDeclaration(current) && current.name) {
return current.name.text;
}
if (typescript_1.default.isMethodDeclaration(current) && typescript_1.default.isIdentifier(current.name)) {
return current.name.text;
}
if (typescript_1.default.isVariableDeclaration(current) &&
typescript_1.default.isIdentifier(current.name) &&
current.initializer &&
(typescript_1.default.isFunctionExpression(current.initializer) ||
typescript_1.default.isArrowFunction(current.initializer))) {
return current.name.text;
}
current = current.parent;
}
return undefined;
}
}
exports.RedosPatternDetector = RedosPatternDetector;
RedosPatternDetector.REDOS_PATTERNS = [
{
id: "regex-exponential-backtrack",
name: "Exponential backtracking regex",
description: "Regular expression with exponential backtracking pattern detected - vulnerable to ReDoS attacks",
pattern: {
type: "regex",
expression: /\([^)]*\+[^)]*\)\+|\([^)]*\*[^)]*\)\*|\([^)]*\+[^)]*\)\{/g,
},
vulnerabilityType: "redos-vulnerability",
severity: "high",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "regex-nested-quantifiers",
name: "Nested quantifiers regex",
description: "Regular expression with nested quantifiers detected - may cause ReDoS",
pattern: {
type: "regex",
expression: /\([^)]*[\+\*]\)[^)]*[\+\*]|\([^)]*\{[^}]*\}[^)]*\)[^)]*[\+\*]/g,
},
vulnerabilityType: "redos-vulnerability",
severity: "medium",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
];
// Known dangerous regex patterns that cause exponential backtracking
RedosPatternDetector.DANGEROUS_PATTERNS = [
// Classic exponential patterns
/^\(a\+\)\+$/, // (a+)+
/^\(a\*\)\*$/, // (a*)*
/^\(a\|\*\)\*$/, // (a|*)*
/^\(a\|a\)\*$/, // (a|a)*
// Nested quantifiers
/\([^)]*\+[^)]*\)\+/, // (pattern+)+
/\([^)]*\*[^)]*\)\*/, // (pattern*)*
/\([^)]*\+[^)]*\)\{/, // (pattern+){n,m}
/\([^)]*\*[^)]*\)\{/, // (pattern*){n,m}
// Alternation with overlap
/\([^|)]*\|[^|)]*\)[\+\*]/, // (a|b)+
// Complex nested structures
/\([^)]*\([^)]*[\+\*][^)]*\)[^)]*\)[\+\*]/, // nested groups with quantifiers
// Catastrophic backtracking patterns
/\^[^$]*\([^)]*\+[^)]*\)[^$]*\$/, // ^...(pattern+)...$
];