UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

396 lines (395 loc) 17.7 kB
"use strict"; /** * Detector for missing security headers in Next.js configuration */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecurityHeaderDetector = 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"); class SecurityHeaderDetector extends BaseDetector_1.BaseDetector { constructor() { super("SecurityHeaderDetector", "missing-security-headers", "medium", SecurityHeaderDetector.SECURITY_HEADER_PATTERNS); } async detect(scanResult, context) { const vulnerabilities = []; // DEBUG: Look for any config-related files const allConfigFiles = scanResult.filePaths.filter((filePath) => filePath.includes("next.config")); // DEBUG: Check a few sample file paths to understand the structure const samplePaths = scanResult.filePaths.slice(0, 5); // DEBUG: Check if we can find any .ts files at root level const rootTsFiles = scanResult.filePaths.filter((filePath) => { const normalized = filePath.replace(/\\/g, "/"); const parts = normalized.split("/"); return (parts[parts.length - 1].endsWith(".ts") && !normalized.includes("/")); }); // Look specifically for Next.js config files const configFiles = scanResult.filePaths.filter((filePath) => /next\.config\.(js|ts|mjs)$/.test(filePath)); if (configFiles.length === 0) { // Check if this is actually a Next.js project before flagging const hasNextJsIndicators = this.isNextJsProject(scanResult); if (hasNextJsIndicators) { vulnerabilities.push(this.createMissingConfigVulnerability(context.projectPath)); } return vulnerabilities; } for (const configPath of configFiles) { const content = scanResult.fileContents.get(configPath); if (!content) { continue; } // Analyze the config file for security headers const configAnalysis = this.analyzeNextConfigFile(configPath, scanResult); vulnerabilities.push(...configAnalysis); } return vulnerabilities; } /** * Check if this is actually a Next.js project */ isNextJsProject(scanResult) { // Check for package.json with Next.js dependency const packageJsonPath = scanResult.filePaths.find((path) => path.endsWith("package.json") && !path.includes("node_modules")); if (packageJsonPath) { const packageContent = scanResult.fileContents.get(packageJsonPath); if (packageContent && packageContent.includes('"next"')) { return true; } } // Check for Next.js specific files/directories return scanResult.filePaths.some((path) => general_constants_1.NEXTJS_INDICATORS.some((indicator) => path.includes(indicator))); } /** * Check if security headers might be configured elsewhere (e.g., CDN, reverse proxy) */ hasAlternativeSecurityConfig(scanResult) { // Check for deployment config files that might handle security headers return scanResult.filePaths.some((path) => general_constants_1.DEPLOYMENT_CONFIGS.some((config) => path.includes(config))); } /** * Analyze Next.js config file for security headers */ analyzeNextConfigFile(filePath, scanResult) { const vulnerabilities = []; // Parse the config file const sourceFile = scanResult.sourceFiles.get(filePath); if (!sourceFile) { return vulnerabilities; } // Check if security headers might be configured elsewhere const hasAlternativeConfig = this.hasAlternativeSecurityConfig(scanResult); // Find security headers configuration const headersConfig = this.findHeadersConfiguration(sourceFile); const missingHeaders = this.identifyMissingSecurityHeaders(headersConfig); const insecureHeaders = this.identifyInsecureHeaders(headersConfig); // Create vulnerabilities for missing headers (with reduced severity if alternative config exists) for (const missingHeader of missingHeaders) { const vulnerability = this.createMissingHeaderVulnerability(filePath, missingHeader); // Reduce severity if headers might be configured elsewhere if (hasAlternativeConfig) { vulnerability.severity = "low"; vulnerability.metadata = { ...vulnerability.metadata, note: "Security headers might be configured at infrastructure level", }; } vulnerabilities.push(vulnerability); } // Create vulnerabilities for insecure headers for (const insecureHeader of insecureHeaders) { const vulnerability = this.createInsecureHeaderVulnerability(filePath, insecureHeader); vulnerabilities.push(vulnerability); } return vulnerabilities; } /** * Find headers configuration in Next.js config */ findHeadersConfiguration(sourceFile) { const configObject = this.findNextConfigObject(sourceFile); if (!configObject) return null; // Look for headers property const headersProperty = this.findPropertyInObject(configObject, "headers"); if (!headersProperty) return null; return this.parseHeadersConfiguration(headersProperty, sourceFile); } /** * Find the main Next.js config object */ findNextConfigObject(sourceFile) { // Look for module.exports = {...} or export default {...} const exportAssignments = ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.BinaryExpression, (node) => { return (typescript_1.default.isPropertyAccessExpression(node.left) && typescript_1.default.isIdentifier(node.left.expression) && node.left.expression.text === "module" && typescript_1.default.isIdentifier(node.left.name) && node.left.name.text === "exports"); }); for (const assignment of exportAssignments) { if (typescript_1.default.isObjectLiteralExpression(assignment.right)) { return assignment.right; } } // Look for export default const exportDefaults = ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.ExportAssignment); for (const exportDefault of exportDefaults) { if (typescript_1.default.isObjectLiteralExpression(exportDefault.expression)) { return exportDefault.expression; } } return null; } /** * Find a property in an object literal */ findPropertyInObject(obj, propertyName) { for (const prop of obj.properties) { if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name) && prop.name.text === propertyName) { return prop; } } return null; } /** * Parse headers configuration */ parseHeadersConfiguration(headersProperty, sourceFile) { const config = { configuredHeaders: new Map(), hasHeadersFunction: false, location: ASTTraverser_1.ASTTraverser.getNodeLocation(headersProperty, sourceFile), }; // Check if headers is a function if (typescript_1.default.isFunctionExpression(headersProperty.initializer) || typescript_1.default.isArrowFunction(headersProperty.initializer)) { config.hasHeadersFunction = true; // Analyze function body for header configurations this.analyzeHeadersFunction(headersProperty.initializer, config, sourceFile); } else if (typescript_1.default.isArrayLiteralExpression(headersProperty.initializer)) { // Direct array of header configurations this.analyzeHeadersArray(headersProperty.initializer, config, sourceFile); } return config; } /** * Analyze headers function */ analyzeHeadersFunction(func, config, sourceFile) { // Look for return statements with header arrays within this specific function const returnStatements = ASTTraverser_1.ASTTraverser.findNodesByKindInNode(func, typescript_1.default.SyntaxKind.ReturnStatement); for (const returnStmt of returnStatements) { if (returnStmt.expression && typescript_1.default.isArrayLiteralExpression(returnStmt.expression)) { this.analyzeHeadersArray(returnStmt.expression, config, sourceFile); } } } /** * Analyze headers array */ analyzeHeadersArray(array, config, sourceFile) { for (const element of array.elements) { if (typescript_1.default.isObjectLiteralExpression(element)) { this.analyzeHeaderObject(element, config, sourceFile); } } } /** * Analyze individual header object */ analyzeHeaderObject(obj, config, sourceFile) { const headersProperty = this.findPropertyInObject(obj, "headers"); if (!headersProperty || !typescript_1.default.isArrayLiteralExpression(headersProperty.initializer)) { return; } for (const headerElement of headersProperty.initializer.elements) { if (typescript_1.default.isObjectLiteralExpression(headerElement)) { const keyProp = this.findPropertyInObject(headerElement, "key"); const valueProp = this.findPropertyInObject(headerElement, "value"); if (keyProp && valueProp && typescript_1.default.isStringLiteral(keyProp.initializer) && typescript_1.default.isStringLiteral(valueProp.initializer)) { const headerName = keyProp.initializer.text; const headerValue = valueProp.initializer.text; config.configuredHeaders.set(headerName, { value: headerValue, location: ASTTraverser_1.ASTTraverser.getNodeLocation(headerElement, sourceFile), }); } } } } /** * Identify missing security headers */ identifyMissingSecurityHeaders(config) { const missing = []; for (const [headerName, headerInfo] of Object.entries(SecurityHeaderDetector.REQUIRED_SECURITY_HEADERS)) { const isConfigured = config?.configuredHeaders.has(headerName) || config?.configuredHeaders.has(headerName.toLowerCase()); if (!isConfigured) { missing.push({ headerName, headerInfo, severity: headerInfo.severity, }); } } return missing; } /** * Fixed identifyInsecureHeaders function with proper type safety */ identifyInsecureHeaders(config) { const insecure = []; if (!config) return insecure; for (const [headerName, configuredHeader] of config.configuredHeaders) { const requiredHeader = SecurityHeaderDetector.REQUIRED_SECURITY_HEADERS[headerName]; if (!requiredHeader) continue; const isSecure = this.isHeaderValueSecure(configuredHeader.value, requiredHeader.recommendedValue, requiredHeader.alternativeValues); if (!isSecure) { insecure.push({ headerName, currentValue: configuredHeader.value, recommendedValue: requiredHeader.recommendedValue, location: configuredHeader.location, severity: requiredHeader.severity, }); } } return insecure; } /** * Check if header value is secure */ isHeaderValueSecure(currentValue, recommendedValue, alternativeValues) { if (currentValue === recommendedValue) return true; return alternativeValues.includes(currentValue); } /** * Create vulnerability for missing Next.js config file */ createMissingConfigVulnerability(projectPath) { return this.createVulnerability(projectPath, { line: 1, column: 1 }, { code: "// Missing next.config.js file", surroundingContext: "Next.js project root directory", }, "Next.js project missing security headers configuration - create next.config.js with security headers", "medium", "high", { missingFile: "next.config.js", recommendation: "Create next.config.js with security headers configuration", requiredHeaders: Object.keys(SecurityHeaderDetector.REQUIRED_SECURITY_HEADERS), severity: "medium", // Reduced from previous implementation }); } /** * Create vulnerability for missing security header */ createMissingHeaderVulnerability(filePath, missingHeader) { // Adjust severity based on header importance const adjustedSeverity = missingHeader.headerInfo.severity === "high" ? "medium" : "low"; return this.createVulnerability(filePath, { line: 1, column: 1 }, { code: `// Missing ${missingHeader.headerName} header`, surroundingContext: "Next.js configuration file", }, `Missing security header: ${missingHeader.headerName} - ${missingHeader.headerInfo.description}`, adjustedSeverity, "medium", { headerName: missingHeader.headerName, description: missingHeader.headerInfo.description, recommendedValue: missingHeader.headerInfo.recommendedValue, headerType: "missing", }); } /** * Create vulnerability for insecure security header */ createInsecureHeaderVulnerability(filePath, insecureHeader) { return this.createVulnerability(filePath, insecureHeader.location, { code: `${insecureHeader.headerName}: ${insecureHeader.currentValue}`, surroundingContext: "Next.js headers configuration", }, `Insecure ${insecureHeader.headerName} header value - current: "${insecureHeader.currentValue}", recommended: "${insecureHeader.recommendedValue}"`, insecureHeader.severity, "high", { headerName: insecureHeader.headerName, currentValue: insecureHeader.currentValue, recommendedValue: insecureHeader.recommendedValue, headerType: "insecure", }); } } exports.SecurityHeaderDetector = SecurityHeaderDetector; SecurityHeaderDetector.SECURITY_HEADER_PATTERNS = [ { id: "next-config-file", name: "Next.js configuration file", description: "Next.js configuration file detected - checking for security headers", pattern: { type: "regex", expression: /next\.config\.(js|ts|mjs)$/g, // Already correct, but ensure the filter includes all }, vulnerabilityType: "missing-security-headers", severity: "medium", confidence: "high", fileTypes: [".js", ".ts", ".mjs"], // Add .ts and .mjs enabled: true, }, ]; SecurityHeaderDetector.REQUIRED_SECURITY_HEADERS = { "X-Frame-Options": { name: "X-Frame-Options", description: "Prevents clickjacking attacks by controlling frame embedding", recommendedValue: "DENY", alternativeValues: ["SAMEORIGIN"], severity: "medium", }, "X-Content-Type-Options": { name: "X-Content-Type-Options", description: "Prevents MIME type sniffing attacks", recommendedValue: "nosniff", alternativeValues: [], severity: "medium", }, "Referrer-Policy": { name: "Referrer-Policy", description: "Controls referrer information sent in requests", recommendedValue: "strict-origin-when-cross-origin", alternativeValues: ["no-referrer", "same-origin", "strict-origin"], severity: "medium", }, "X-XSS-Protection": { name: "X-XSS-Protection", description: "Enables XSS filtering in older browsers", recommendedValue: "1; mode=block", alternativeValues: ["0"], severity: "low", }, "Strict-Transport-Security": { name: "Strict-Transport-Security", description: "Enforces HTTPS connections", recommendedValue: "max-age=31536000; includeSubDomains", alternativeValues: ["max-age=31536000"], severity: "high", }, "Content-Security-Policy": { name: "Content-Security-Policy", description: "Prevents XSS and other injection attacks", recommendedValue: "default-src 'self'", alternativeValues: [], severity: "high", }, "Permissions-Policy": { name: "Permissions-Policy", description: "Controls browser feature permissions", recommendedValue: "camera=(), microphone=(), geolocation=()", alternativeValues: [], severity: "low", }, };