UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

284 lines (283 loc) 12.4 kB
"use strict"; /** * Detector for environment variable exposure in client-side code */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnvironmentExposureDetector = void 0; const typescript_1 = __importDefault(require("typescript")); const BaseDetector_1 = require("./BaseDetector"); const ASTTraverser_1 = require("../utils/ASTTraverser"); const sensitiveData_constants_1 = require("../constants/sensitiveData.constants"); const environment_constants_1 = require("../constants/environment.constants"); const security_constants_1 = require("../constants/security.constants"); class EnvironmentExposureDetector extends BaseDetector_1.BaseDetector { constructor() { super("EnvironmentExposureDetector", "environment-exposure", "high", EnvironmentExposureDetector.ENV_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"]); for (const filePath of relevantFiles) { const content = scanResult.fileContents.get(filePath); if (!content) continue; // Determine if this is a client-side file const fileContext = this.getFileContext(filePath, content); // Skip server-side files (API routes, middleware, etc.) if (!fileContext.isClientSide || fileContext.fileType === "api-route" || fileContext.fileType === "middleware") { continue; } // Apply pattern matching const patternResults = this.applyPatternMatching(content, filePath); const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateEnvMatch(match, fileContext.isClientSide)); // Apply AST-based analysis const sourceFile = scanResult.sourceFiles.get(filePath); if (sourceFile) { const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForEnvExposure(sf, fp, fileContext.isClientSide)); vulnerabilities.push(...astVulnerabilities); } // Process pattern vulnerabilities for (const vuln of patternVulnerabilities) { vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext); if (this.validateVulnerability(vuln)) { vulnerabilities.push(vuln); } } } return vulnerabilities; } /** * Validate if an environment variable match is problematic */ validateEnvMatch(matchResult, isClientSide) { 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; } // Only flag client-side usage if (!isClientSide) { return false; } // Extract environment variable name const envVar = this.extractEnvVariableName(match.match); if (!envVar) return false; // Check if it's a known safe client variable if (this.isClientSafeEnvVar(envVar)) { return false; } return true; } /** * AST-based analysis for environment variable exposure */ analyzeASTForEnvExposure(sourceFile, filePath, isClientSide) { const vulnerabilities = []; if (!isClientSide) { return vulnerabilities; } // Find all property access expressions for process.env const processEnvAccess = this.findProcessEnvAccess(sourceFile); for (const envAccess of processEnvAccess) { const envVarName = this.getEnvVariableName(envAccess); if (envVarName) { const context = ASTTraverser_1.ASTTraverser.getNodeContext(envAccess, sourceFile); // Check if NODE_ENV usage is properly gated if (envVarName === "NODE_ENV" && this.isProperlyGatedForDevelopment(envVarName, context)) { continue; // Skip properly gated NODE_ENV usage } const riskAssessment = this.assessEnvVariableRisk(envVarName); if (riskAssessment) { const location = ASTTraverser_1.ASTTraverser.getNodeLocation(envAccess, sourceFile); const code = ASTTraverser_1.ASTTraverser.getNodeText(envAccess, 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(envAccess), }, riskAssessment.description, "high", riskAssessment.confidence, { envVariableName: envVarName, riskLevel: riskAssessment.riskLevel, isServerOnly: riskAssessment.isServerOnly, suggestion: riskAssessment.suggestion, detectionMethod: "ast-analysis", }); vulnerabilities.push(vulnerability); } } } return vulnerabilities; } /** * Find all process.env property access expressions */ findProcessEnvAccess(sourceFile) { const propertyAccess = ASTTraverser_1.ASTTraverser.findPropertyAccess(sourceFile); return propertyAccess.filter((propAccess) => { // Check for process.env.VARIABLE pattern if (typescript_1.default.isPropertyAccessExpression(propAccess.expression) && typescript_1.default.isIdentifier(propAccess.expression.expression) && typescript_1.default.isIdentifier(propAccess.expression.name) && propAccess.expression.expression.text === "process" && propAccess.expression.name.text === "env") { return true; } return false; }); } /** * Get environment variable name from property access */ getEnvVariableName(propAccess) { if (typescript_1.default.isIdentifier(propAccess.name)) { return propAccess.name.text; } return null; } /** * Assess the risk of using an environment variable in client code */ assessEnvVariableRisk(envVarName) { // Check if it's a known client-safe variable if (this.isClientSafeEnvVar(envVarName)) { return null; } // Check if it's a known server-only variable const isServerOnly = this.isServerOnlyEnvVar(envVarName); if (isServerOnly) { return { description: `Server-only environment variable '${envVarName}' accessed in client code - this will be undefined or may expose sensitive data`, confidence: "high", riskLevel: "critical", isServerOnly: true, suggestion: `Move to server-side code or use NEXT_PUBLIC_ prefix if safe for client exposure`, }; } // Check if it looks sensitive based on naming const isSensitive = this.looksLikeSensitiveEnvVar(envVarName); if (isSensitive) { return { description: `Potentially sensitive environment variable '${envVarName}' accessed in client code - verify this is safe for client exposure`, confidence: "medium", riskLevel: "high", isServerOnly: false, suggestion: `Verify this is safe for client exposure or move to server-side code`, }; } // Unknown variable - flag for review return { description: `Environment variable '${envVarName}' accessed in client code - verify this is safe for client exposure`, confidence: "low", riskLevel: "medium", isServerOnly: false, suggestion: `Review if this environment variable is safe for client exposure`, }; } /** * Extract environment variable name from match string */ extractEnvVariableName(match) { const envMatch = match.match(/process\.env\.(\w+)/); return envMatch ? envMatch[1] : null; } /** * Check if environment variable is safe for client-side use */ isClientSafeEnvVar(envVarName) { return environment_constants_1.CLIENT_SAFE_ENV_VARS.some((safe) => envVarName.startsWith(safe) || envVarName === safe); } /** * Check if environment variable is server-only */ isServerOnlyEnvVar(envVarName) { return environment_constants_1.SERVER_ONLY_ENV_VARS.includes(envVarName); } /** * Check if environment variable name looks sensitive */ looksLikeSensitiveEnvVar(envVarName) { const upperName = envVarName.toUpperCase(); return sensitiveData_constants_1.ENV_SENSITIVE_KEYWORDS.some((keyword) => upperName.includes(keyword)); } /** * Check if environment variable usage is properly gated for development */ isProperlyGatedForDevelopment(envVarName, context) { // Only apply gating check for NODE_ENV if (envVarName !== "NODE_ENV") { return false; } const cleanContext = context.replace(/\s+/g, " ").trim(); // Check if it's used in a proper development gating pattern return security_constants_1.DEVELOPMENT_GATING_PATTERNS.some((pattern) => pattern.test(cleanContext)); } /** * 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.EnvironmentExposureDetector = EnvironmentExposureDetector; EnvironmentExposureDetector.ENV_PATTERNS = [ { id: "process-env-access", name: "process.env access in client code", description: "process.env access detected in client-side code - may expose server-only environment variables", pattern: { type: "regex", expression: /process\.env\.\w+/g, }, vulnerabilityType: "environment-exposure", severity: "high", confidence: "medium", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, { id: "server-env-in-client", name: "Server environment variable in client code", description: "Server-only environment variable accessed in client code - this will be undefined or expose sensitive data", pattern: { type: "regex", expression: /process\.env\.(DATABASE_URL|DB_PASSWORD|SECRET_KEY|PRIVATE_KEY|API_SECRET)/g, }, vulnerabilityType: "environment-exposure", severity: "high", confidence: "high", fileTypes: [".ts", ".tsx", ".js", ".jsx"], enabled: true, }, ];