UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

421 lines (420 loc) 18 kB
"use strict"; /** * Detector for insecure cookie patterns and configurations */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InsecureCookieDetector = void 0; const typescript_1 = __importDefault(require("typescript")); const BaseDetector_1 = require("./BaseDetector"); const ASTTraverser_1 = require("../utils/ASTTraverser"); const storage_constants_1 = require("../constants/storage.constants"); const general_constants_1 = require("../constants/general.constants"); class InsecureCookieDetector extends BaseDetector_1.BaseDetector { constructor() { super("InsecureCookieDetector", "insecure-cookie", "medium", InsecureCookieDetector.COOKIE_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; // Check if file uses cookie libraries const cookieLibraries = this.detectCookieLibraries(content); // Apply pattern matching for document.cookie usage const patternResults = this.applyPatternMatching(content, filePath); const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateCookieMatch(match)); // Apply AST-based analysis const sourceFile = scanResult.sourceFiles.get(filePath); if (sourceFile) { const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForInsecureCookies(sf, fp, cookieLibraries)); 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"; } // Add library information if detected if (cookieLibraries.length > 0) { vuln.metadata = { ...vuln.metadata, cookieLibraries, note: "Cookie libraries detected - verify their security configuration", }; } vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext); if (this.validateVulnerability(vuln)) { vulnerabilities.push(vuln); } } } return vulnerabilities; } /** * Detect cookie libraries used in the file */ detectCookieLibraries(content) { const foundLibraries = []; for (const lib of storage_constants_1.COOKIE_LIBRARIES) { if (content.includes(lib)) { foundLibraries.push(lib); } } return foundLibraries; } /** * Validate if a cookie pattern match is problematic */ validateCookieMatch(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; } // Check if it's in test code if (this.isInTestContext(match.context || "")) { return false; } return true; } /** * Check if cookie usage is for non-sensitive UI state */ isUIStateCookie(cookieString, context) { const lowerCookie = cookieString.toLowerCase(); const lowerContext = context.toLowerCase(); // Check cookie name/content for UI state indicators const isUIStateCookie = general_constants_1.UI_COOKIE_STATE_PATTERNS.some((pattern) => lowerCookie.includes(pattern) || lowerContext.includes(pattern)); // Check if it's a framework UI state cookie const isFrameworkUI = general_constants_1.FRAMEWORK_UI_PATTERNS.some((pattern) => lowerCookie.includes(pattern)); return isUIStateCookie || isFrameworkUI; } /** * AST-based analysis for insecure cookie usage */ analyzeASTForInsecureCookies(sourceFile, filePath, cookieLibraries) { const vulnerabilities = []; // Find document.cookie assignments const cookieAssignments = this.findDocumentCookieAssignments(sourceFile); for (const assignment of cookieAssignments) { const cookieVuln = this.analyzeCookieAssignment(assignment, sourceFile, filePath); if (cookieVuln) { vulnerabilities.push(cookieVuln); } } // Find cookie library usage const cookieLibraryCalls = this.findCookieLibraryCalls(sourceFile, cookieLibraries); for (const libCall of cookieLibraryCalls) { const libVuln = this.analyzeCookieLibraryCall(libCall, sourceFile, filePath); if (libVuln) { vulnerabilities.push(libVuln); } } return vulnerabilities; } /** * Find document.cookie assignments */ findDocumentCookieAssignments(sourceFile) { return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.BinaryExpression, (node) => { return (node.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken && typescript_1.default.isPropertyAccessExpression(node.left) && typescript_1.default.isIdentifier(node.left.expression) && node.left.expression.text === "document" && typescript_1.default.isIdentifier(node.left.name) && node.left.name.text === "cookie"); }); } /** * Find cookie library function calls */ findCookieLibraryCalls(sourceFile, cookieLibraries) { if (cookieLibraries.length === 0) return []; return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => { // Look for library.set() or library() calls if (typescript_1.default.isPropertyAccessExpression(node.expression)) { const obj = node.expression.expression; const method = node.expression.name; if (typescript_1.default.isIdentifier(obj) && typescript_1.default.isIdentifier(method)) { return (cookieLibraries.some((lib) => obj.text.includes(lib.replace("-", "")) || obj.text.toLowerCase().includes("cookie")) && (method.text === "set" || method.text === "setItem")); } } return false; }); } /** * Analyze document.cookie assignment */ analyzeCookieAssignment(assignment, sourceFile, filePath) { const location = ASTTraverser_1.ASTTraverser.getNodeLocation(assignment, sourceFile); const context = ASTTraverser_1.ASTTraverser.getNodeContext(assignment, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(assignment, sourceFile); // Extract cookie string const cookieString = this.extractCookieString(assignment.right); if (!cookieString) return null; // Check if it's UI state cookie (should have lower severity) const isUIState = this.isUIStateCookie(cookieString, context); // Analyze cookie security const securityAnalysis = this.analyzeCookieSecurity(cookieString); if (securityAnalysis.issues.length === 0) { return null; } // If it's UI state, reduce severity unless it has serious security issues const severity = isUIState ? "low" : "medium"; const confidence = isUIState ? "low" : "medium"; const issueDescription = securityAnalysis.issues.join(", "); return this.createVulnerability(filePath, { line: location.line, column: location.column, endLine: location.line, endColumn: location.column + code.length, }, { code, surroundingContext: context, functionName: this.extractFunctionFromAST(assignment), }, `Cookie configuration detected: ${issueDescription}${isUIState ? " (UI state cookie)" : ""}`, severity, confidence, { cookieString, securityIssues: securityAnalysis.issues, missingAttributes: securityAnalysis.missingAttributes, recommendations: securityAnalysis.recommendations, isUIState: isUIState, detectionMethod: "document-cookie-analysis", }); } /** * Analyze cookie library call */ analyzeCookieLibraryCall(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); // Analyze the options object (usually the third argument) const optionsAnalysis = this.analyzeCookieOptions(callExpr); if (!optionsAnalysis || optionsAnalysis.issues.length === 0) { return null; } const issueDescription = optionsAnalysis.issues.join(", "); 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), }, `Insecure cookie library configuration: ${issueDescription}`, "medium", "medium", { libraryCall: true, securityIssues: optionsAnalysis.issues, missingAttributes: optionsAnalysis.missingAttributes, recommendations: optionsAnalysis.recommendations, detectionMethod: "cookie-library-analysis", }); } /** * Extract cookie string from assignment */ extractCookieString(expr) { if (typescript_1.default.isStringLiteral(expr)) { return expr.text; } if (typescript_1.default.isTemplateExpression(expr)) { // Try to extract static parts return expr.head.text; } return null; } /** * Analyze cookie security attributes */ analyzeCookieSecurity(cookieString) { const analysis = { issues: [], missingAttributes: [], recommendations: [], }; const lowerCookie = cookieString.toLowerCase(); // Check if this appears to be a UI state cookie const isUIState = this.isUIStateCookie(cookieString, ""); // For UI state cookies, be less strict about security attributes if (isUIState) { // Only flag critical missing attributes for UI cookies if (!lowerCookie.includes("samesite")) { analysis.issues.push("missing SameSite flag"); analysis.missingAttributes.push("SameSite"); analysis.recommendations.push("Add 'SameSite=Lax' to prevent CSRF attacks"); } return analysis; } // For potentially sensitive cookies, check all security attributes if (!lowerCookie.includes("secure")) { analysis.issues.push("missing Secure flag"); analysis.missingAttributes.push("Secure"); analysis.recommendations.push("Add 'Secure' flag to ensure cookie is only sent over HTTPS"); } if (!lowerCookie.includes("httponly")) { analysis.issues.push("missing HttpOnly flag"); analysis.missingAttributes.push("HttpOnly"); analysis.recommendations.push("Add 'HttpOnly' flag to prevent XSS attacks"); } if (!lowerCookie.includes("samesite")) { analysis.issues.push("missing SameSite flag"); analysis.missingAttributes.push("SameSite"); analysis.recommendations.push("Add 'SameSite=Strict' or 'SameSite=Lax' to prevent CSRF attacks"); } return analysis; } /** * Analyze cookie options object from library calls */ analyzeCookieOptions(callExpr) { // Usually options are in the third argument for cookie.set(name, value, options) if (callExpr.arguments.length < 3) { return { issues: ["no security options provided"], missingAttributes: ["Secure", "HttpOnly", "SameSite"], recommendations: [ "Provide security options object with Secure, HttpOnly, and SameSite attributes", ], }; } const optionsArg = callExpr.arguments[2]; if (!typescript_1.default.isObjectLiteralExpression(optionsArg)) { return null; } const analysis = { issues: [], missingAttributes: [], recommendations: [], }; const optionProperties = new Set(); for (const prop of optionsArg.properties) { if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name)) { optionProperties.add(prop.name.text.toLowerCase()); } } // Check for required attributes if (!optionProperties.has("secure")) { analysis.issues.push("missing secure option"); analysis.missingAttributes.push("secure"); analysis.recommendations.push("Add 'secure: true' option"); } if (!optionProperties.has("httponly")) { analysis.issues.push("missing httpOnly option"); analysis.missingAttributes.push("httpOnly"); analysis.recommendations.push("Add 'httpOnly: true' option"); } if (!optionProperties.has("samesite")) { analysis.issues.push("missing sameSite option"); analysis.missingAttributes.push("sameSite"); analysis.recommendations.push("Add 'sameSite: \"Strict\"' or 'sameSite: \"Lax\"' option"); } return analysis; } /** * 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.InsecureCookieDetector = InsecureCookieDetector; InsecureCookieDetector.COOKIE_PATTERNS = [ { id: "document-cookie-assignment", name: "document.cookie assignment", description: "document.cookie assignment detected - verify secure cookie attributes", pattern: { type: "regex", expression: /document\.cookie\s*=/g, }, vulnerabilityType: "insecure-cookie", severity: "medium", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "cookie-without-secure", name: "Cookie without Secure flag", description: "Cookie set without Secure flag - should be used in HTTPS context", pattern: { type: "regex", expression: /document\.cookie\s*=\s*[^;]*(?!.*[Ss]ecure)/g, }, vulnerabilityType: "insecure-cookie", severity: "medium", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "cookie-without-httponly", name: "Cookie without HttpOnly flag", description: "Cookie set without HttpOnly flag - vulnerable to XSS attacks", pattern: { type: "regex", expression: /document\.cookie\s*=\s*[^;]*(?!.*[Hh]ttp[Oo]nly)/g, }, vulnerabilityType: "insecure-cookie", severity: "medium", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "cookie-without-samesite", name: "Cookie without SameSite flag", description: "Cookie set without SameSite flag - vulnerable to CSRF attacks", pattern: { type: "regex", expression: /document\.cookie\s*=\s*[^;]*(?!.*[Ss]ame[Ss]ite)/g, }, vulnerabilityType: "insecure-cookie", severity: "medium", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, ];