UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

457 lines (456 loc) 19.7 kB
"use strict"; /** * Detector for client-side storage of sensitive data */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientStorageDetector = 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 sensitiveData_constants_1 = require("../constants/sensitiveData.constants"); class ClientStorageDetector extends BaseDetector_1.BaseDetector { constructor() { super("ClientStorageDetector", "client-storage-sensitive", "medium", ClientStorageDetector.STORAGE_PATTERNS); } async detect(scanResult) { const vulnerabilities = []; // Filter relevant files (focus on client-side 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; // Only analyze client-side files const fileContext = this.getFileContext(filePath, content); if (!fileContext.isClientSide || fileContext.fileType === "api-route" || fileContext.fileType === "middleware") { continue; } // Check for storage libraries const storageLibraries = this.detectStorageLibraries(content); // Apply pattern matching const patternResults = this.applyPatternMatching(content, filePath); const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateStorageMatch(match)); // Apply AST-based analysis const sourceFile = scanResult.sourceFiles.get(filePath); if (sourceFile) { const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForClientStorage(sf, fp, storageLibraries)); vulnerabilities.push(...astVulnerabilities); } // Process pattern vulnerabilities for (const vuln of patternVulnerabilities) { // Add library information if detected if (storageLibraries.length > 0) { vuln.metadata = { ...vuln.metadata, storageLibraries, note: "Storage libraries detected - verify their security configuration", }; } vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext); if (this.validateVulnerability(vuln)) { vulnerabilities.push(vuln); } } } return vulnerabilities; } /** * Detect storage libraries used in the file */ detectStorageLibraries(content) { const foundLibraries = []; for (const lib of storage_constants_1.STORAGE_LIBRARIES) { if (content.includes(lib)) { foundLibraries.push(lib); } } return foundLibraries; } /** * Validate if a storage pattern match is problematic */ validateStorageMatch(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; } /** * AST-based analysis for client storage usage */ analyzeASTForClientStorage(sourceFile, filePath, storageLibraries) { const vulnerabilities = []; // Find storage API calls const storageApiCalls = this.findStorageApiCalls(sourceFile); for (const apiCall of storageApiCalls) { const apiVuln = this.analyzeStorageApiCall(apiCall, sourceFile, filePath); if (apiVuln) { vulnerabilities.push(apiVuln); } } // Find storage library calls const storageLibraryCalls = this.findStorageLibraryCalls(sourceFile, storageLibraries); for (const libCall of storageLibraryCalls) { const libVuln = this.analyzeStorageLibraryCall(libCall, sourceFile, filePath); if (libVuln) { vulnerabilities.push(libVuln); } } return vulnerabilities; } /** * Find storage API calls (localStorage, sessionStorage, etc.) */ findStorageApiCalls(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(obj) && typescript_1.default.isIdentifier(method)) { return (storage_constants_1.STORAGE_APIS.includes(obj.text) && (method.text === "setItem" || method.text === "getItem" || method.text === "removeItem" || method.text === "clear")); } } return false; }); } /** * Find storage library calls */ findStorageLibraryCalls(sourceFile, storageLibraries) { if (storageLibraries.length === 0) return []; return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => { // Look for library method 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)) { const objName = obj.text.toLowerCase(); return (storageLibraries.some((lib) => objName.includes(lib.replace("-", "")) || objName.includes("storage") || objName.includes("db")) && (method.text === "set" || method.text === "get" || method.text === "put" || method.text === "add")); } } return false; }); } /** * Analyze storage API call */ analyzeStorageApiCall(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); // Get storage type and method const storageInfo = this.getStorageInfo(callExpr); if (!storageInfo) return null; // Analyze the key and value for sensitive data const sensitivityAnalysis = this.analyzeStorageSensitivity(callExpr); if (!sensitivityAnalysis || sensitivityAnalysis.sensitivityLevel === "none") { return null; } // Check if this appears to be UI state storage const keyArg = callExpr.arguments[0]; if (keyArg && typescript_1.default.isStringLiteral(keyArg)) { if (this.isUIStateStorage(keyArg.text, context)) { return null; // Skip UI state storage } } 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), }, `${storageInfo.storageType}.${storageInfo.method}() used with potentially sensitive data: ${sensitivityAnalysis.sensitiveKeys.join(", ")}`, "medium", sensitivityAnalysis.confidence, { storageType: storageInfo.storageType, method: storageInfo.method, sensitiveKeys: sensitivityAnalysis.sensitiveKeys, sensitivityLevel: sensitivityAnalysis.sensitivityLevel, recommendations: sensitivityAnalysis.recommendations, detectionMethod: "storage-api-analysis", }); } /** * Analyze storage library call */ analyzeStorageLibraryCall(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 for sensitive data patterns const sensitivityAnalysis = this.analyzeStorageSensitivity(callExpr); if (!sensitivityAnalysis || sensitivityAnalysis.sensitivityLevel === "none") { 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(callExpr), }, `Storage library used with potentially sensitive data: ${sensitivityAnalysis.sensitiveKeys.join(", ")}`, "medium", sensitivityAnalysis.confidence, { libraryCall: true, sensitiveKeys: sensitivityAnalysis.sensitiveKeys, sensitivityLevel: sensitivityAnalysis.sensitivityLevel, recommendations: sensitivityAnalysis.recommendations, detectionMethod: "storage-library-analysis", }); } /** * Get storage type and method information */ getStorageInfo(callExpr) { if (typescript_1.default.isPropertyAccessExpression(callExpr.expression)) { const obj = callExpr.expression.expression; const method = callExpr.expression.name; if (typescript_1.default.isIdentifier(obj) && typescript_1.default.isIdentifier(method)) { return { storageType: obj.text, method: method.text, }; } } return null; } /** * Analyze storage call for sensitive data */ analyzeStorageSensitivity(callExpr) { const analysis = { sensitiveKeys: [], sensitivityLevel: "none", confidence: "low", recommendations: [], }; // Analyze arguments for sensitive data for (let i = 0; i < callExpr.arguments.length; i++) { const arg = callExpr.arguments[i]; const sensitiveData = this.extractSensitiveDataFromArgument(arg); if (sensitiveData.length > 0) { analysis.sensitiveKeys.push(...sensitiveData); } } if (analysis.sensitiveKeys.length === 0) { return null; } // Determine sensitivity level and confidence analysis.sensitivityLevel = this.determineSensitivityLevel(analysis.sensitiveKeys); analysis.confidence = this.determineConfidenceLevel(analysis.sensitiveKeys); analysis.recommendations = this.generateStorageRecommendations(analysis.sensitiveKeys); return analysis; } /** * Extract sensitive data indicators from function argument */ extractSensitiveDataFromArgument(arg) { const sensitiveData = []; if (typescript_1.default.isStringLiteral(arg)) { const text = arg.text.toLowerCase(); // Check if it's explicitly non-sensitive or UI state if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => text.includes(term))) { return []; // Return empty array for non-sensitive terms } if (storage_constants_1.UI_STATE_TERMS.some((term) => text.includes(term))) { return []; // Return empty for UI state } const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => text.includes(keyword)); sensitiveData.push(...foundSensitive); } else if (typescript_1.default.isIdentifier(arg)) { const name = arg.text.toLowerCase(); if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => name.includes(term))) { return []; } if (storage_constants_1.UI_STATE_TERMS.some((term) => name.includes(term))) { return []; } const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => name.includes(keyword)); sensitiveData.push(...foundSensitive); } else if (typescript_1.default.isPropertyAccessExpression(arg)) { const propertyName = typescript_1.default.isIdentifier(arg.name) ? arg.name.text.toLowerCase() : ""; if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => propertyName.includes(term))) { return []; } const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => propertyName.includes(keyword)); sensitiveData.push(...foundSensitive); } return sensitiveData; } /** * Check if storage usage is for UI state rather than sensitive data */ isUIStateStorage(key, context) { const lowerKey = key.toLowerCase(); const lowerContext = context.toLowerCase(); // Check if key or context suggests UI state const isUIKey = storage_constants_1.UI_STATE_PATTERNS.some((pattern) => lowerKey.includes(pattern) || lowerContext.includes(pattern)); // Additional context checks for common UI patterns const hasUIContext = lowerContext.includes("component") || lowerContext.includes("hook") || lowerContext.includes("use") || lowerContext.includes("state") || lowerContext.includes("local"); return isUIKey || hasUIContext; } /** * Determine sensitivity level based on found keywords */ determineSensitivityLevel(sensitiveKeys) { if (sensitiveKeys.some((key) => sensitiveData_constants_1.HIGH_SENSITIVITY_KEYWORDS.includes(key))) { return "high"; } else if (sensitiveKeys.some((key) => sensitiveData_constants_1.MEDIUM_SENSITIVITY_KEYWORDS.includes(key))) { return "medium"; } else if (sensitiveKeys.length > 0) { return "low"; } return "none"; } /** * Determine confidence level based on context */ determineConfidenceLevel(sensitiveKeys) { if (sensitiveKeys.some((key) => sensitiveData_constants_1.CLIENT_EXPLICIT_SENSITIVE.includes(key))) { return "high"; } else if (sensitiveKeys.length > 1) { return "medium"; } return "low"; } /** * Generate storage security recommendations */ generateStorageRecommendations(sensitiveKeys) { const recommendations = []; if (sensitiveKeys.includes("password") || sensitiveKeys.includes("secret")) { recommendations.push("Never store passwords or secrets in client-side storage"); recommendations.push("Use secure server-side session management instead"); } if (sensitiveKeys.includes("token") || sensitiveKeys.includes("jwt")) { recommendations.push("Consider using httpOnly cookies for token storage"); recommendations.push("Implement token rotation and expiration"); } if (sensitiveKeys.length > 0) { recommendations.push("Encrypt sensitive data before storing"); recommendations.push("Use sessionStorage instead of localStorage for temporary data"); recommendations.push("Implement data cleanup on logout"); } return recommendations; } /** * 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.ClientStorageDetector = ClientStorageDetector; ClientStorageDetector.STORAGE_PATTERNS = [ { id: "localstorage-sensitive", name: "localStorage with sensitive data", description: "localStorage usage with potentially sensitive data detected", pattern: { type: "regex", expression: /localStorage\.(setItem|getItem)\s*\([^)]*(?:password|token|secret|key|auth|jwt|session)[^)]*/gi, }, vulnerabilityType: "client-storage-sensitive", severity: "medium", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "sessionstorage-sensitive", name: "sessionStorage with sensitive data", description: "sessionStorage usage with potentially sensitive data detected", pattern: { type: "regex", expression: /sessionStorage\.(setItem|getItem)\s*\([^)]*(?:password|token|secret|key|auth|jwt|session)[^)]*/gi, }, vulnerabilityType: "client-storage-sensitive", severity: "medium", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "indexeddb-sensitive", name: "IndexedDB with sensitive data", description: "IndexedDB usage with potentially sensitive data detected", pattern: { type: "regex", expression: /indexedDB\.|IDBDatabase/gi, }, vulnerabilityType: "client-storage-sensitive", severity: "medium", confidence: "low", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, ];