UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

458 lines (457 loc) 18 kB
"use strict"; /** * Detector for mixed content vulnerabilities (HTTP resources in HTTPS context) */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MixedContentDetector = void 0; const typescript_1 = __importDefault(require("typescript")); const BaseDetector_1 = require("./BaseDetector"); const ASTTraverser_1 = require("../utils/ASTTraverser"); const general_constants_1 = require("../constants/general.constants"); const network_constants_1 = require("../constants/network.constants"); const security_constants_1 = require("../constants/security.constants"); class MixedContentDetector extends BaseDetector_1.BaseDetector { constructor() { super("MixedContentDetector", "mixed-content", "high", MixedContentDetector.MIXED_CONTENT_PATTERNS); } async detect(scanResult) { const vulnerabilities = []; // Filter relevant files // TODO: MOVE TO CONSTANTS 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; // Apply pattern matching const patternResults = this.applyPatternMatching(content, filePath); const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateMixedContentMatch(match)); // Apply AST-based analysis for more sophisticated detection const sourceFile = scanResult.sourceFiles.get(filePath); if (sourceFile) { const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForMixedContent(sf, fp)); vulnerabilities.push(...astVulnerabilities); } // Adjust confidence based on file context const fileContext = this.getFileContext(filePath, content); for (const vuln of patternVulnerabilities) { // Higher confidence for client-side files if (fileContext.isClientSide) { vuln.confidence = "high"; } // Lower confidence for server-side files if (fileContext.riskContexts.includes("server-side")) { vuln.confidence = "medium"; } vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext); if (this.validateVulnerability(vuln)) { vulnerabilities.push(vuln); } } } return vulnerabilities; } /** * Validate if a mixed content match is actually risky */ validateMixedContentMatch(matchResult) { const match = matchResult.matches[0]; if (!match) return false; // Check if it's in a comment if (this.isInComment(match.context || "", match.match)) { return false; } // Extract URL from the match const url = this.extractURLFromMatch(match.match); if (!url) return false; // Allow localhost and development URLs if (this.isAllowedHTTPContext(url)) { return false; } // Check if it's actually HTTP (not HTTPS) if (!url.startsWith("http://")) { return false; } return true; } /** * AST-based analysis for mixed content detection */ analyzeASTForMixedContent(sourceFile, filePath) { const vulnerabilities = []; // Find string literals with HTTP URLs const httpStrings = this.findHTTPStringLiterals(sourceFile); for (const stringLiteral of httpStrings) { const url = stringLiteral.text; // Skip allowed contexts if (this.isAllowedHTTPContext(url)) { continue; } const riskAssessment = this.assessMixedContentRisk(stringLiteral, sourceFile); if (riskAssessment) { const location = ASTTraverser_1.ASTTraverser.getNodeLocation(stringLiteral, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(stringLiteral, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(stringLiteral, sourceFile); const vulnerability = this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(stringLiteral), }, riskAssessment.description, "high", riskAssessment.confidence, { url, usageContext: riskAssessment.usageContext, riskLevel: riskAssessment.riskLevel, detectionMethod: "ast-analysis", }); vulnerabilities.push(vulnerability); } } // Find JSX attributes with HTTP URLs const jsxHTTPAttributes = this.findJSXHTTPAttributes(sourceFile); for (const jsxAttr of jsxHTTPAttributes) { const attrVuln = this.analyzeJSXHTTPAttribute(jsxAttr, sourceFile, filePath); if (attrVuln) { vulnerabilities.push(attrVuln); } } return vulnerabilities; } /** * Find string literals containing HTTP URLs */ findHTTPStringLiterals(sourceFile) { const httpPattern = /^http?:\/\//; return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.StringLiteral, (node) => httpPattern.test(node.text)); } /** * Find JSX attributes with HTTP URLs */ findJSXHTTPAttributes(sourceFile) { const jsxAttributes = []; const jsxElements = ASTTraverser_1.ASTTraverser.findJSXElements(sourceFile); for (const jsxElement of jsxElements) { const attributes = typescript_1.default.isJsxElement(jsxElement) ? jsxElement.openingElement.attributes.properties : jsxElement.attributes.properties; for (const attr of attributes) { if (typescript_1.default.isJsxAttribute(attr) && attr.initializer) { if (typescript_1.default.isStringLiteral(attr.initializer) && attr.initializer.text.startsWith("http://")) { jsxAttributes.push(attr); } else if (typescript_1.default.isJsxExpression(attr.initializer) && attr.initializer.expression && typescript_1.default.isStringLiteral(attr.initializer.expression) && attr.initializer.expression.text.startsWith("http://")) { jsxAttributes.push(attr); } } } } return jsxAttributes; } /** * Assess the risk of mixed content for a string literal */ assessMixedContentRisk(stringLiteral, sourceFile) { const url = stringLiteral.text; // Determine usage context const usageContext = this.determineURLUsageContext(stringLiteral, sourceFile); if (!usageContext) { return null; } // Assess risk based on usage context const riskLevel = this.getRiskLevel(usageContext, url); const confidence = this.getConfidenceLevel(usageContext); return { description: `HTTP URL '${url}' used in ${usageContext} - this may cause mixed content issues in HTTPS context`, confidence, usageContext, riskLevel, }; } /** * Determine how a URL is being used */ determineURLUsageContext(stringLiteral, sourceFile) { const parent = stringLiteral.parent; // Check if it's a fetch call if (typescript_1.default.isCallExpression(parent) && typescript_1.default.isIdentifier(parent.expression) && parent.expression.text === "fetch") { return "fetch-api-call"; } // Check if it's an axios call if (typescript_1.default.isCallExpression(parent) && typescript_1.default.isPropertyAccessExpression(parent.expression) && typescript_1.default.isIdentifier(parent.expression.expression) && parent.expression.expression.text === "axios") { return "axios-api-call"; } // Check if it's a variable assignment const variableName = this.getVariableNameForStringLiteral(stringLiteral); if (variableName && this.isURLRelatedVariable(variableName)) { return `variable-assignment-${variableName}`; } // Check if it's a property assignment if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) { const propertyName = parent.name.text; if (this.isURLRelatedProperty(propertyName)) { return `property-assignment-${propertyName}`; } } return null; } /** * Analyze JSX HTTP attribute */ analyzeJSXHTTPAttribute(jsxAttr, sourceFile, filePath) { if (!typescript_1.default.isIdentifier(jsxAttr.name) || !jsxAttr.initializer) { return null; } const attrName = jsxAttr.name.text; const url = this.getJSXAttributeURL(jsxAttr); if (!url || this.isAllowedHTTPContext(url)) { return null; } // Skip XML namespace declarations if (attrName === "xmlns" && this.isXMLNamespace(url)) { return null; } const location = ASTTraverser_1.ASTTraverser.getNodeLocation(jsxAttr, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(jsxAttr, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(jsxAttr, sourceFile); const riskLevel = this.getJSXAttributeRiskLevel(attrName); return this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(jsxAttr), }, `HTTP URL in JSX ${attrName} attribute - this will be blocked in HTTPS context`, "high", riskLevel === "critical" ? "high" : "medium", { attributeName: attrName, url, riskLevel, detectionMethod: "jsx-analysis", }); } /** * Get URL from JSX attribute */ getJSXAttributeURL(jsxAttr) { if (!jsxAttr.initializer) return null; if (typescript_1.default.isStringLiteral(jsxAttr.initializer)) { return jsxAttr.initializer.text; } else if (typescript_1.default.isJsxExpression(jsxAttr.initializer) && jsxAttr.initializer.expression && typescript_1.default.isStringLiteral(jsxAttr.initializer.expression)) { return jsxAttr.initializer.expression.text; } return null; } /** * Get risk level for JSX attribute */ getJSXAttributeRiskLevel(attrName) { if (general_constants_1.CRITICAL_HTML_ATTRIBUTES.includes(attrName)) { return "critical"; } else if (general_constants_1.HIGH_RISK_HTML_ATTRIBUTES.includes(attrName)) { return "high"; } return "medium"; } /** * Extract URL from pattern match */ extractURLFromMatch(match) { const urlMatch = match.match(/https?:\/\/[^'"`\s]+/); return urlMatch ? urlMatch[0] : null; } /** * Check if HTTP context is allowed (development/localhost) */ isAllowedHTTPContext(url) { // Check standard XML namespaces first if (security_constants_1.SAFE_XML_NAMESPACES.includes(url)) { return true; } // Check development/localhost contexts return network_constants_1.ALLOWED_HTTP_CONTEXTS.some((allowed) => url.includes(allowed)); } /** * Check if URL is a standard XML namespace declaration */ isXMLNamespace(url) { return security_constants_1.SAFE_XML_NAMESPACES.includes(url); } /** * Get variable name for string literal */ getVariableNameForStringLiteral(node) { let parent = node.parent; if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) { return parent.name.text; } if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) { return parent.name.text; } return undefined; } /** * Check if variable name is URL-related */ isURLRelatedVariable(name) { const lowerName = name.toLowerCase(); return network_constants_1.RISKY_HTTP_CONTEXTS.some((context) => lowerName.includes(context)); } /** * Check if property name is URL-related */ isURLRelatedProperty(name) { return this.isURLRelatedVariable(name); } /** * Get risk level based on usage context */ getRiskLevel(usageContext, url) { if (usageContext.includes("api-call") || usageContext.includes("fetch")) { return "high"; } if (usageContext.includes("src") || usageContext.includes("href")) { return "critical"; } return "medium"; } /** * Get confidence level based on usage context */ getConfidenceLevel(usageContext) { if (usageContext.includes("api-call") || usageContext.includes("fetch") || usageContext.includes("src") || usageContext.includes("href")) { return "high"; } return "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.MixedContentDetector = MixedContentDetector; MixedContentDetector.MIXED_CONTENT_PATTERNS = [ { id: "http-url-string", name: "HTTP URL in string literal", description: "HTTP URL detected - this may cause mixed content issues in HTTPS context", pattern: { type: "regex", expression: /['"`]https?:\/\/[^'"`\s]+['"`]/g, }, vulnerabilityType: "mixed-content", severity: "high", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "http-api-call", name: "HTTP API endpoint", description: "HTTP API endpoint detected - ensure HTTPS is used in production", pattern: { type: "regex", expression: /fetch\s*\(\s*['"`]http:\/\/[^'"`\s]+['"`]/g, }, vulnerabilityType: "mixed-content", severity: "high", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "http-axios-call", name: "HTTP Axios request", description: "HTTP Axios request detected - ensure HTTPS is used in production", pattern: { type: "regex", expression: /axios\.[a-z]+\s*\(\s*['"`]http:\/\/[^'"`\s]+['"`]/g, }, vulnerabilityType: "mixed-content", severity: "high", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "http-image-src", name: "HTTP image source", description: "HTTP image source detected - this will be blocked in HTTPS context", pattern: { type: "regex", expression: /src\s*=\s*['"`]http:\/\/[^'"`\s]+\.(jpg|jpeg|png|gif|svg|webp)['"`]/gi, }, vulnerabilityType: "mixed-content", severity: "high", confidence: "high", fileTypes: [".tsx", ".jsx"], enabled: true, }, { id: "http-script-src", name: "HTTP script source", description: "HTTP script source detected - this will be blocked in HTTPS context", pattern: { type: "regex", expression: /src\s*=\s*['"`]http:\/\/[^'"`\s]+\.js['"`]/gi, }, vulnerabilityType: "mixed-content", severity: "high", confidence: "high", fileTypes: [".tsx", ".jsx"], enabled: true, }, ];