UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

330 lines (329 loc) 13.7 kB
"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+)...$ ];