UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

530 lines (529 loc) 22.9 kB
"use strict"; /** * Detector for SQL injection vulnerabilities */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlInjectionDetector = void 0; const typescript_1 = __importDefault(require("typescript")); const BaseDetector_1 = require("./BaseDetector"); const ASTTraverser_1 = require("../utils/ASTTraverser"); const sql_constants_1 = require("../constants/sql.constants"); class SqlInjectionDetector extends BaseDetector_1.BaseDetector { constructor() { super("SqlInjectionDetector", "sql-injection", "critical", SqlInjectionDetector.SQL_PATTERNS); } async detect(scanResult) { const vulnerabilities = []; 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 analyze files that actually use SQL libraries or contain SQL patterns const sqlLibraries = this.detectSqlLibraries(content); const hasSqlContent = this.hasSqlContent(content); if (sqlLibraries.length === 0 && !hasSqlContent) { continue; // Skip files without SQL relevance } // Apply pattern matching const patternResults = this.applyPatternMatching(content, filePath); const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateSqlMatch(match)); // Apply AST-based analysis const sourceFile = scanResult.sourceFiles.get(filePath); if (sourceFile) { const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForSqlInjection(sf, fp, sqlLibraries)); vulnerabilities.push(...astVulnerabilities); } // Adjust confidence based on file context const fileContext = this.getFileContext(filePath, content); for (const vuln of patternVulnerabilities) { if (sqlLibraries.length > 0) { vuln.metadata = { ...vuln.metadata, sqlLibraries, }; } vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext); if (this.validateVulnerability(vuln)) { vulnerabilities.push(vuln); } } } return vulnerabilities; } /** * Detect SQL libraries used in the file */ detectSqlLibraries(content) { const foundLibraries = []; for (const lib of sql_constants_1.SQL_LIBRARIES) { if (content.includes(lib)) { foundLibraries.push(lib); } } return foundLibraries; } /** * Check if content has SQL-related patterns */ hasSqlContent(content) { // Look for SQL keywords in combination with execution patterns const sqlKeywordPattern = new RegExp(`(?:query|execute|exec|sql)\\s*\\([^)]*(?:${sql_constants_1.SQL_KEYWORDS.join("|")})`, "gi"); return (sqlKeywordPattern.test(content) || content.includes("pool.query") || content.includes("db.query") || content.includes("connection.query")); } /** * Validate if a SQL pattern match is problematic */ validateSqlMatch(matchResult) { const match = matchResult.matches[0]; if (!match) return false; if (this.isInComment(match.context || "", match.match)) { return false; } if (this.isInTestContext(match.context || "")) { return false; } return true; } /** * AST-based analysis for SQL injection patterns */ analyzeASTForSqlInjection(sourceFile, filePath, sqlLibraries) { const vulnerabilities = []; // Find SQL execution calls const sqlCalls = this.findSqlExecutionCalls(sourceFile); for (const sqlCall of sqlCalls) { const sqlVuln = this.analyzeSqlCall(sqlCall, sourceFile, filePath); if (sqlVuln) { vulnerabilities.push(sqlVuln); } } // Find template literals with SQL content const templateLiterals = this.findSqlTemplateLiterals(sourceFile); for (const template of templateLiterals) { const templateVuln = this.analyzeTemplateLiteral(template, sourceFile, filePath); if (templateVuln) { vulnerabilities.push(templateVuln); } } // Find variable assignments with actual SQL content (more selective) const sqlVariables = this.findActualSqlVariableAssignments(sourceFile); for (const sqlVar of sqlVariables) { const varVuln = this.analyzeSqlVariable(sqlVar, sourceFile, filePath); if (varVuln) { vulnerabilities.push(varVuln); } } return vulnerabilities; } /** * Find SQL execution method calls */ findSqlExecutionCalls(sourceFile) { return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => { if (typescript_1.default.isPropertyAccessExpression(node.expression)) { const obj = node.expression.expression; const method = node.expression.name; if (typescript_1.default.isIdentifier(method)) { // Check for database-specific method calls if (sql_constants_1.SQL_EXECUTION_METHODS.includes(method.text) || sql_constants_1.RAW_SQL_METHODS.includes(method.text)) { if (typescript_1.default.isIdentifier(obj)) { // Only flag if object suggests database connection const objName = obj.text.toLowerCase(); return (objName.includes("pool") || objName.includes("db") || objName.includes("connection") || objName.includes("client") || objName.includes("database")); } return true; } } } else if (typescript_1.default.isIdentifier(node.expression)) { return sql_constants_1.SQL_EXECUTION_METHODS.includes(node.expression.text); } return false; }); } /** * Find template literals that contain SQL keywords */ findSqlTemplateLiterals(sourceFile) { return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.TemplateExpression, (node) => { const templateText = ASTTraverser_1.ASTTraverser.getNodeText(node, sourceFile); const upperText = templateText.toUpperCase(); // Must contain SQL keywords and variables to be suspicious const hasSqlKeywords = sql_constants_1.SQL_KEYWORDS.some((keyword) => upperText.includes(keyword)); const hasVariables = templateText.includes("${"); return hasSqlKeywords && hasVariables; }); } /** * Find variable assignments that actually contain SQL queries (more selective) */ findActualSqlVariableAssignments(sourceFile) { return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.VariableDeclaration, (node) => { if (!node.initializer) return false; const initText = ASTTraverser_1.ASTTraverser.getNodeText(node.initializer, sourceFile); const upperInitText = initText.toUpperCase(); // ADDITION: Skip React components and UI functions if (typescript_1.default.isIdentifier(node.name)) { const varName = node.name.text; // Skip React components (PascalCase starting with capital) if (/^[A-Z][a-zA-Z0-9]*$/.test(varName)) { return false; } // Skip common UI/React function patterns const uiFunctionPatterns = [ "component", "hook", "render", "get", "handle", "on", "use", "icon", "button", "form", "input", "modal", "page", "layout", ]; const lowerVarName = varName.toLowerCase(); if (uiFunctionPatterns.some((pattern) => lowerVarName.includes(pattern))) { return false; } } // ADDITION: Skip if initializer is clearly a function/component if (typescript_1.default.isArrowFunction(node.initializer) || typescript_1.default.isFunctionExpression(node.initializer)) { return false; } // Only flag if: // 1. Variable name suggests SQL AND initializer contains SQL keywords // 2. OR initializer clearly contains SQL query with dynamic content let isLikelySql = false; // Check if variable name suggests SQL if (typescript_1.default.isIdentifier(node.name)) { const varName = node.name.text.toLowerCase(); const isSqlVariableName = sql_constants_1.SQL_VARIABLE_NAMES.some((name) => varName.includes(name)); if (isSqlVariableName) { // Variable name suggests SQL, check if content has SQL keywords isLikelySql = sql_constants_1.SQL_KEYWORDS.some((keyword) => upperInitText.includes(keyword)); } } // OR check if initializer clearly contains SQL with dynamic content if (!isLikelySql) { const hasSqlKeywords = sql_constants_1.SQL_KEYWORDS.slice(0, 6).some((keyword // Focus on main keywords ) => upperInitText.includes(keyword)); const hasDynamicContent = initText.includes("${") || initText.includes(" + ") || initText.includes(".concat("); isLikelySql = hasSqlKeywords && hasDynamicContent; } return isLikelySql; }); } /** * Analyze SQL execution call for injection vulnerabilities */ analyzeSqlCall(callExpr, sourceFile, filePath) { const location = ASTTraverser_1.ASTTraverser.getNodeLocation(callExpr, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(callExpr, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(callExpr, sourceFile); const methodName = this.getSqlMethodName(callExpr); if (!methodName) return null; // Check if it's a safe ORM method if (sql_constants_1.SAFE_ORM_METHODS.includes(methodName)) { return null; } // Analyze arguments for user input const injectionAnalysis = this.analyzeSqlArguments(callExpr, sourceFile); if (!injectionAnalysis || injectionAnalysis.userInputSources.length === 0) { return null; } // Check if query uses parameterization const hasParameterization = this.hasParameterization(code); const confidence = hasParameterization ? "low" : injectionAnalysis.confidence; return this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(callExpr), }, `SQL execution method '${methodName}()' with user input: ${injectionAnalysis.userInputSources.join(", ")}`, sql_constants_1.RAW_SQL_METHODS.includes(methodName) ? "critical" : "high", confidence, { method: methodName, userInputSources: injectionAnalysis.userInputSources, hasParameterization, isRawSql: sql_constants_1.RAW_SQL_METHODS.includes(methodName), detectionMethod: "sql-call-analysis", }); } /** * Analyze template literal for SQL injection */ analyzeTemplateLiteral(template, sourceFile, filePath) { const location = ASTTraverser_1.ASTTraverser.getNodeLocation(template, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(template, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(template, sourceFile); // Check if template spans contain user input const userInputSources = this.extractUserInputFromTemplate(template, sourceFile); if (userInputSources.length === 0) { return null; } return this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(template), }, `SQL template literal with user input: ${userInputSources.join(", ")}`, "critical", "high", { userInputSources, templateType: "sql-query", detectionMethod: "template-literal-analysis", }); } /** * Analyze SQL variable assignment */ analyzeSqlVariable(varDecl, sourceFile, filePath) { if (!varDecl.initializer) return null; const location = ASTTraverser_1.ASTTraverser.getNodeLocation(varDecl, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(varDecl, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(varDecl, sourceFile); // Check for string concatenation or template literals with user input const hasUnsafeConstruction = this.hasUnsafeStringConstruction(varDecl.initializer, sourceFile); if (!hasUnsafeConstruction) { return null; } const varName = typescript_1.default.isIdentifier(varDecl.name) ? varDecl.name.text : "unknown"; return this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(varDecl), }, `SQL query variable '${varName}' constructed unsafely`, "high", "medium", { variableName: varName, constructionMethod: "string-manipulation", detectionMethod: "variable-analysis", }); } /** * Get SQL method name from call expression */ getSqlMethodName(callExpr) { if (typescript_1.default.isPropertyAccessExpression(callExpr.expression) && typescript_1.default.isIdentifier(callExpr.expression.name)) { return callExpr.expression.name.text; } else if (typescript_1.default.isIdentifier(callExpr.expression)) { return callExpr.expression.text; } return null; } /** * Analyze SQL call arguments for user input */ analyzeSqlArguments(callExpr, sourceFile) { const analysis = { userInputSources: [], confidence: "low", }; for (const arg of callExpr.arguments) { const inputSources = this.extractUserInputFromExpression(arg, sourceFile); analysis.userInputSources.push(...inputSources); } if (analysis.userInputSources.length > 0) { analysis.confidence = this.determineConfidenceLevel(analysis.userInputSources); return analysis; } return null; } /** * Extract user input sources from expression */ extractUserInputFromExpression(expr, sourceFile) { const userInputSources = []; if (typescript_1.default.isPropertyAccessExpression(expr)) { const propAccess = this.getPropertyAccessPath(expr); if (propAccess && sql_constants_1.SQL_INJECTION_INPUT_SOURCES.some((source) => propAccess.toLowerCase().includes(source.toLowerCase()))) { userInputSources.push(propAccess); } } else if (typescript_1.default.isIdentifier(expr)) { const name = expr.text.toLowerCase(); if (sql_constants_1.SQL_INJECTION_INPUT_SOURCES.some((source) => name.includes(source.toLowerCase()))) { userInputSources.push(name); } } else if (typescript_1.default.isTemplateExpression(expr)) { for (const span of expr.templateSpans) { const spanSources = this.extractUserInputFromExpression(span.expression, sourceFile); userInputSources.push(...spanSources); } } return userInputSources; } /** * Extract user input from template literal spans */ extractUserInputFromTemplate(template, sourceFile) { const userInputSources = []; for (const span of template.templateSpans) { const sources = this.extractUserInputFromExpression(span.expression, sourceFile); userInputSources.push(...sources); } return userInputSources; } /** * Get property access path as string */ getPropertyAccessPath(expr) { const path = []; let current = expr; while (typescript_1.default.isPropertyAccessExpression(current)) { if (typescript_1.default.isIdentifier(current.name)) { path.unshift(current.name.text); } current = current.expression; } if (typescript_1.default.isIdentifier(current)) { path.unshift(current.text); } return path.length > 0 ? path.join(".") : null; } /** * Check if query uses parameterization */ hasParameterization(code) { return sql_constants_1.SAFE_QUERY_PATTERNS.some((pattern) => pattern.test(code)); } /** * Check if expression uses unsafe string construction */ hasUnsafeStringConstruction(expr, sourceFile) { const exprText = ASTTraverser_1.ASTTraverser.getNodeText(expr, sourceFile); // Check for template literals with variables if (typescript_1.default.isTemplateExpression(expr)) { return true; // Template expressions with variables are potentially unsafe } // Check for string concatenation return sql_constants_1.STRING_CONCAT_PATTERNS.some((pattern) => pattern.test(exprText)); } /** * Determine confidence level based on input sources */ determineConfidenceLevel(inputSources) { const hasDirectInput = inputSources.some((source) => source.includes("req.") || source.includes("request.") || source.includes("query") || source.includes("params") || source.includes("body")); return hasDirectInput ? "high" : "medium"; } /** * 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.SqlInjectionDetector = SqlInjectionDetector; SqlInjectionDetector.SQL_PATTERNS = [ { id: "sql-template-literal", name: "SQL template literal with variables", description: "SQL query constructed using template literals with variables - potential SQL injection", pattern: { type: "regex", expression: /`[^`]*\$\{[^}]*\}[^`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|UNION|WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|JOIN|FROM|INTO|VALUES|SET|LIMIT|OFFSET)/gi, }, vulnerabilityType: "sql-injection", severity: "critical", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "sql-string-concatenation", name: "SQL string concatenation", description: "SQL query constructed using string concatenation - potential SQL injection", pattern: { type: "regex", expression: /['"][^'"]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|UNION|WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|JOIN|FROM|INTO|VALUES|SET|LIMIT|OFFSET)[^'"]*['"]\s*\+/gi, }, vulnerabilityType: "sql-injection", severity: "critical", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "raw-sql-with-variables", name: "Raw SQL execution with variables", description: "Raw SQL execution method with dynamic content - verify parameterization", pattern: { type: "regex", expression: /(?:query|execute|exec|raw|sql)\s*\(\s*[^)]*\$\{|(?:query|execute|exec|raw|sql)\s*\(\s*[^)]*\+/gi, }, vulnerabilityType: "sql-injection", severity: "high", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, ];