sicua
Version:
A tool for analyzing project structure and dependencies
396 lines (395 loc) • 17.7 kB
JavaScript
"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",
},
};