UNPKG

vibe-guard

Version:

██ Vibe-Guard Security Scanner - 28 essential security rules to catch vulnerabilities before they catch you! Zero dependencies, instant setup, works everywhere, optimized performance. Detects SQL injection, XSS, exposed secrets, CSRF, CORS issues, contain

634 lines 26.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MissingSecurityHeadersRule = void 0; const types_1 = require("../types"); class MissingSecurityHeadersRule extends types_1.BaseRule { constructor() { super(...arguments); this.name = 'missing-security-headers'; this.description = 'Detects missing HTTP security headers with context-aware analysis'; this.severity = 'medium'; this.securityHeaders = [ { name: 'Content-Security-Policy', severity: 'critical', description: 'Critical security header to prevent XSS attacks', validation: (content) => this.validateCSP(content), suggestion: 'Implement Content-Security-Policy to prevent XSS attacks' }, { name: 'Strict-Transport-Security', severity: 'critical', description: 'Critical security header to enforce HTTPS connections', validation: (content) => this.validateHSTS(content), suggestion: 'Implement HSTS to enforce HTTPS connections' }, { name: 'X-Frame-Options', severity: 'high', description: 'High priority header to prevent clickjacking attacks', validation: (content) => this.validateXFrameOptions(content), suggestion: 'Set X-Frame-Options to prevent clickjacking attacks' }, { name: 'Referrer-Policy', severity: 'high', description: 'High priority header to control referrer information', validation: (content) => this.validateReferrerPolicy(content), suggestion: 'Set Referrer-Policy to control referrer information' }, { name: 'X-Content-Type-Options', severity: 'medium', description: 'Medium priority header to prevent MIME type sniffing', validation: (content) => this.validateXContentTypeOptions(content), suggestion: 'Set X-Content-Type-Options to nosniff to prevent MIME type sniffing' }, { name: 'Permissions-Policy', severity: 'medium', description: 'Medium priority header to control browser features', validation: (content) => this.validatePermissionsPolicy(content), suggestion: 'Implement Permissions-Policy to control browser features' }, { name: 'X-XSS-Protection', severity: 'low', description: 'Legacy header for XSS protection (optional)', isLegacy: true, validation: (content) => this.validateXXSSProtection(content), suggestion: 'X-XSS-Protection is legacy and optional. Consider using Content-Security-Policy instead' }, { name: 'X-Permitted-Cross-Domain-Policies', severity: 'low', description: 'Low priority header to control cross-domain access', validation: (content) => this.validateXPermittedCrossDomainPolicies(content), suggestion: 'Set X-Permitted-Cross-Domain-Policies to control cross-domain access' } ]; this.serverPatterns = [ // Express.js patterns { pattern: /\bapp\.(?:get|post|put|delete|patch|use)\s*\(/gi, type: 'Express route handler', framework: 'express' }, { pattern: /\broter\.(?:get|post|put|delete|patch|use)\s*\(/gi, type: 'Express router', framework: 'express' }, { pattern: /\bapp\.listen\s*\(/gi, type: 'Express server', framework: 'express' }, // Next.js API routes { pattern: /\bexport\s+(?:default\s+)?(?:async\s+)?function\s+handler/gi, type: 'Next.js API handler', framework: 'nextjs' }, { pattern: /\bexport\s+(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\([^)]*req[^)]*res[^)]*\)/gi, type: 'Next.js API function', framework: 'nextjs' }, // Node.js HTTP server { pattern: /\bcreateServer\s*\(\s*(?:async\s+)?\([^)]*req[^)]*res[^)]*\)/gi, type: 'Node.js HTTP server', framework: 'nodejs' }, { pattern: /\bhttp\.createServer/gi, type: 'HTTP server creation', framework: 'nodejs' }, // Framework response patterns { pattern: /\bres\.(?:send|json|render|redirect)/gi, type: 'Response method', framework: 'express' }, { pattern: /\bresponse\.(?:send|json|render|redirect)/gi, type: 'Response method', framework: 'general' }, // Flask patterns { pattern: /\b@app\.route/gi, type: 'Flask route', framework: 'flask' }, { pattern: /\breturn\s+(?:render_template|jsonify|redirect)/gi, type: 'Flask response', framework: 'flask' }, // Django patterns { pattern: /\bdef\s+\w+\s*\([^)]*request[^)]*\)/gi, type: 'Django view function', framework: 'django' }, { pattern: /\bHttpResponse\s*\(/gi, type: 'Django HTTP response', framework: 'django' }, // Rails patterns { pattern: /\bclass\s+\w+Controller\s*</gi, type: 'Rails controller', framework: 'rails' }, { pattern: /\bdef\s+\w+\s*#\s*action/gi, type: 'Rails action', framework: 'rails' }, { pattern: /\brender\s+(?:json|html|xml)/gi, type: 'Rails response', framework: 'rails' }, // Spring patterns { pattern: /\b@RestController/gi, type: 'Spring REST controller', framework: 'spring' }, { pattern: /\b@Controller/gi, type: 'Spring controller', framework: 'spring' }, { pattern: /\b@GetMapping|\b@PostMapping|\b@PutMapping|\b@DeleteMapping/gi, type: 'Spring mapping', framework: 'spring' }, // ASP.NET patterns { pattern: /\bpublic\s+class\s+\w+Controller\s*:\s*Controller/gi, type: 'ASP.NET controller', framework: 'aspnet' }, { pattern: /\bpublic\s+(?:async\s+)?\w+\s+\w+\s*\([^)]*\)/gi, type: 'ASP.NET action', framework: 'aspnet' }, // PHP patterns { pattern: /\bheader\s*\(\s*['"`][^'"`]*['"`]/gi, type: 'PHP header function', framework: 'php' }, // Laravel patterns { pattern: /\bRoute::(?:get|post|put|delete|patch)/gi, type: 'Laravel route', framework: 'laravel' }, { pattern: /\breturn\s+(?:response|view|json)/gi, type: 'Laravel response', framework: 'laravel' } ]; this.safePatterns = [ /\bexample\b/i, /\bdemo\b/i, /\btest\b/i, /\bsample\b/i, /\bplaceholder\b/i, /\bdevelopment\b/i, /\bdev\b/i, /\bstaging\b/i, /\blocalhost\b/i, /\b127\.0\.0\.1\b/i, /\bconsole\.log\b/i, /\bconsole\.warn\b/i, /\bconsole\.error\b/i, /\blogger\.(?:log|warn|error|info)\b/i, /\bprint\b/i, /\becho\b/i, /\bprintf\b/i, /\bSystem\.out\.println\b/i, /\bputs\b/i, /\bConsole\.WriteLine\b/i, /\bcomment\b/i, /\bnote\b/i, /\btodo\b/i, /\bfixme\b/i, /\bsecure\b/i, /\bsafe\b/i, /\bprotected\b/i, /\bdefense\b/i, /\bguard\b/i, /\bprevent\b/i, /\bblock\b/i, /\brestrict\b/i, /\bdocumentation\b/i, /\bconfig\b/i, /\bsettings\b/i, /\bREADME\b/i, /\bCHANGELOG\b/i, /\bLICENSE\b/i ]; } check(fileContent) { const issues = []; const language = this.detectLanguage(fileContent.path); const framework = this.detectFramework(fileContent.content, language); const configurationType = this.detectConfigurationType(fileContent.path); const hasServerCode = this.hasServerCode(fileContent.content); const hasSecurityHeaders = this.hasSecurityHeaders(fileContent.content); // Skip if no server code detected and not a configuration file if (!hasServerCode && !configurationType) { return issues; } // Check for missing security headers and report each separately const missingHeaders = this.checkMissingHeaders(fileContent); if (missingHeaders.length > 0) { const location = this.findReportLocation(fileContent); if (location) { const context = this.analyzeContext(fileContent, location.line, location.column, language, framework, hasServerCode, hasSecurityHeaders, configurationType); // Skip if in safe context to prevent false positives if (!this.isSafeContext(context)) { // Report each missing header as a separate issue for (const header of missingHeaders) { const severity = this.determineSeverity(header.severity, context); const suggestion = this.getRemediationMessage(header, context); issues.push(this.createIssue(fileContent.path, location.line, location.column, location.lineContent, `Missing security header: ${header.name} (${header.description})`, suggestion, severity)); } } } } return issues; } analyzeContext(fileContent, line, column, language, framework, hasServerCode, hasSecurityHeaders, configurationType) { const lines = fileContent.lines; const currentLine = lines[line - 1] || ''; const surroundingLines = lines.slice(Math.max(0, line - 3), line + 2); return { isInComment: this.isInComment(currentLine, language), isInString: this.isInString(currentLine, column), isInConfiguration: this.isInConfiguration(surroundingLines), isInTestFile: this.isInTestFile(fileContent.path), isInDocumentation: this.isInDocumentation(surroundingLines), surroundingCode: surroundingLines.join('\n'), language, framework, hasServerCode: hasServerCode || false, hasSecurityHeaders: hasSecurityHeaders || false, configurationType }; } isInComment(line, language) { const trimmed = line.trim(); if (language === 'javascript' || language === 'typescript') { return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'); } if (language === 'python') { return trimmed.startsWith('#'); } if (language === 'php') { return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('#'); } if (language === 'yaml' || language === 'yml') { return trimmed.startsWith('#'); } if (language === 'ini') { return trimmed.startsWith(';') || trimmed.startsWith('#'); } return false; } isInString(line, column) { const before = line.substring(0, column); const quotes = (before.match(/['"`]/g) || []).length; return quotes % 2 === 1; } isInConfiguration(lines) { return lines.some(line => line.includes('=') || line.includes(':') || line.includes('[') || line.includes('{')); } isInTestFile(filePath) { return filePath.includes('test') || filePath.includes('spec') || filePath.includes('mock'); } isInDocumentation(lines) { return lines.some(line => line.includes('@example') || line.includes('@doc') || line.includes('@description') || line.includes('README') || line.includes('documentation')); } isSafeContext(context) { // Safe if in comment if (context.isInComment) return true; // Safe if in test file if (context.isInTestFile) return true; // Safe if in documentation if (context.isInDocumentation) return true; // Safe if using security-related keywords if (this.safePatterns.some(pattern => pattern.test(context.surroundingCode))) { return true; } return false; } detectLanguage(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'py': 'python', 'php': 'php', 'rb': 'ruby', 'go': 'go', 'java': 'java', 'cs': 'csharp', 'cpp': 'cpp', 'c': 'c', 'rs': 'rust', 'kt': 'kotlin', 'swift': 'swift', 'dart': 'dart', 'scala': 'scala', 'clj': 'clojure', 'hs': 'haskell', 'yaml': 'yaml', 'yml': 'yaml', 'json': 'json', 'ini': 'ini', 'conf': 'conf', 'toml': 'toml', 'env': 'env' }; return languageMap[ext || ''] || 'unknown'; } detectFramework(content, language) { // Check for framework patterns in the content for (const pattern of this.serverPatterns) { if (pattern.pattern.test(content)) { return pattern.framework; } } // Fallback to language-based detection if (language === 'javascript' || language === 'typescript') { if (content.includes('express') || content.includes('app.get') || content.includes('app.post')) return 'express'; if (content.includes('react') || content.includes('jsx') || content.includes('tsx')) return 'react'; if (content.includes('vue') || content.includes('Vue.createApp')) return 'vue'; if (content.includes('angular') || content.includes('@Component')) return 'angular'; if (content.includes('next') || content.includes('Next.js')) return 'nextjs'; } if (language === 'python') { if (content.includes('flask') || content.includes('Flask')) return 'flask'; if (content.includes('django') || content.includes('Django')) return 'django'; if (content.includes('fastapi') || content.includes('FastAPI')) return 'fastapi'; } if (language === 'php') { if (content.includes('laravel') || content.includes('Laravel')) return 'laravel'; if (content.includes('symfony') || content.includes('Symfony')) return 'symfony'; } if (language === 'ruby') { if (content.includes('rails') || content.includes('Rails')) return 'rails'; } if (language === 'java') { if (content.includes('spring') || content.includes('Spring')) return 'spring'; } if (language === 'csharp') { if (content.includes('asp.net') || content.includes('ASP.NET')) return 'aspnet'; } return undefined; } detectConfigurationType(filePath) { const configPatterns = [ /\.conf$/i, /\.config$/i, /\.ini$/i, /\.yaml$/i, /\.yml$/i, /\.json$/i, /\.toml$/i, /\.env$/i, /\.properties$/i, /config\./i, /settings\./i ]; for (const pattern of configPatterns) { if (pattern.test(filePath)) { const ext = filePath.split('.').pop()?.toLowerCase(); return ext || 'unknown'; } } return undefined; } hasServerCode(content) { return this.serverPatterns.some(({ pattern }) => pattern.test(content)); } hasSecurityHeaders(content) { return this.securityHeaders.some(header => this.hasSecurityHeader(content, header.name)); } findMatches(content, pattern) { const matches = []; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; let match; const regex = new RegExp(pattern.source, pattern.flags); while ((match = regex.exec(line)) !== null) { matches.push({ match, line: i + 1, column: match.index + 1, lineContent: line }); } } return matches; } checkMissingHeaders(fileContent) { const missingHeaders = []; for (const header of this.securityHeaders) { if (!this.hasSecurityHeader(fileContent.content, header.name)) { missingHeaders.push(header); } } return missingHeaders; } hasSecurityHeader(content, header) { const headerPatterns = [ // Express.js patterns new RegExp(`res\\.(?:set|header)\\s*\\(\\s*['"\`]${header}['"\`]`, 'gi'), new RegExp(`res\\.setHeader\\s*\\(\\s*['"\`]${header}['"\`]`, 'gi'), // Helmet.js patterns /helmet\s*\(\s*\)/gi, /helmet\./gi, // Manual header setting new RegExp(`['"\`]${header}['"\`]\\s*:\\s*['"\`]`, 'gi'), // PHP patterns new RegExp(`header\\s*\\(\\s*['"\`]${header}:`, 'gi'), // Python Flask patterns new RegExp(`response\\.headers\\[['"\`]${header}['"\`]\\]`, 'gi'), // Django patterns new RegExp(`response\\[['"\`]${header}['"\`]\\]`, 'gi'), // Configuration patterns new RegExp(`${header}\\s*[:=]`, 'gi') ]; return headerPatterns.some(pattern => pattern.test(content)); } findReportLocation(fileContent) { for (const { pattern } of this.serverPatterns) { const matches = this.findMatches(fileContent.content, pattern); if (matches.length > 0) { const firstMatch = matches[0]; if (firstMatch) { return { line: firstMatch.line, column: firstMatch.column, lineContent: firstMatch.lineContent }; } } } return null; } determineSeverity(headerSeverity, context) { // Downgrade severity in development/test contexts instead of skipping if (context.isInTestFile || this.isDevelopmentContext(context)) { switch (headerSeverity) { case 'critical': return 'high'; case 'high': return 'medium'; case 'medium': return 'low'; case 'low': return 'low'; default: return headerSeverity; } } return headerSeverity; } isDevelopmentContext(context) { const devPatterns = [ /\bdevelopment\b/i, /\bdev\b/i, /\bstaging\b/i, /\blocalhost\b/i, /\b127\.0\.0\.1\b/i, /\btest\b/i, /\bmock\b/i, /\bdebug\b/i ]; return devPatterns.some(pattern => pattern.test(context.surroundingCode) || pattern.test(context.configurationType || '')); } getRemediationMessage(header, context) { const framework = context.framework; if (header.isLegacy) { return `${header.suggestion} This header is legacy and may not be supported in modern browsers.`; } let suggestion = header.suggestion; if (framework) { suggestion += this.getFrameworkSpecificSuggestion(header.name, framework); } if (context.configurationType) { suggestion += this.getConfigurationSpecificSuggestion(header.name, context.configurationType); } return suggestion; } getFrameworkSpecificSuggestion(headerName, framework) { const suggestions = { 'Content-Security-Policy': { 'express': ' Use helmet.js: npm install helmet && app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["\'self\'"] } } }))', 'flask': ' Use Flask-Talisman: pip install flask-talisman && talisman.content_security_policy = {"default-src": "\'self\'"}', 'django': ' Add to settings.py: SECURE_CONTENT_TYPE_NOSNIFF = True, SECURE_BROWSER_XSS_FILTER = True', 'rails': ' Add to application.rb: config.content_security_policy do |policy| policy.default_src :self end', 'spring': ' Add to WebSecurityConfig: .headers().contentSecurityPolicy("default-src \'self\'")', 'aspnet': ' Add to Startup.cs: app.Use(async (context, next) => { context.Response.Headers.Add("Content-Security-Policy", "default-src \'self\'"); await next(); })' }, 'Strict-Transport-Security': { 'express': ' Use helmet.js: app.use(helmet({ hsts: { maxAge: 31536000, includeSubDomains: true } }))', 'flask': ' Use Flask-Talisman: talisman.force_https = True', 'django': ' Add to settings.py: SECURE_HSTS_SECONDS = 31536000, SECURE_HSTS_INCLUDE_SUBDOMAINS = True', 'rails': ' Add to application.rb: config.force_ssl = true', 'spring': ' Add to WebSecurityConfig: .headers().httpStrictTransportSecurity()', 'aspnet': ' Add to Startup.cs: app.UseHsts()' }, 'X-Frame-Options': { 'express': ' Use helmet.js: app.use(helmet({ frameguard: { action: "deny" } }))', 'flask': ' Use Flask-Talisman: talisman.frame_options = "DENY"', 'django': ' Add to settings.py: X_FRAME_OPTIONS = "DENY"', 'rails': ' Add to application.rb: config.action_dispatch.default_headers["X-Frame-Options"] = "DENY"', 'spring': ' Add to WebSecurityConfig: .headers().frameOptions().deny()', 'aspnet': ' Add to Startup.cs: app.Use(async (context, next) => { context.Response.Headers.Add("X-Frame-Options", "DENY"); await next(); })' } }; return suggestions[headerName]?.[framework] || ` For ${framework}, implement ${headerName} using framework-specific security middleware.`; } getConfigurationSpecificSuggestion(headerName, configType) { const suggestions = { 'Content-Security-Policy': { 'yaml': ` Add to ${configType} config:\n security:\n headers:\n Content-Security-Policy: "default-src 'self'"`, 'json': ` Add to ${configType} config:\n "security": {\n "headers": {\n "Content-Security-Policy": "default-src 'self'"\n }\n }`, 'ini': ` Add to ${configType} config:\n[security]\nContent-Security-Policy = default-src 'self'` }, 'Strict-Transport-Security': { 'yaml': ` Add to ${configType} config:\n security:\n headers:\n Strict-Transport-Security: "max-age=31536000; includeSubDomains"`, 'json': ` Add to ${configType} config:\n "security": {\n "headers": {\n "Strict-Transport-Security": "max-age=31536000; includeSubDomains"\n }\n }`, 'ini': ` Add to ${configType} config:\n[security]\nStrict-Transport-Security = max-age=31536000; includeSubDomains` } }; return suggestions[headerName]?.[configType] || ` For ${configType} configuration files, ensure proper file permissions and use secure configuration management.`; } // Validation methods for different security headers validateCSP(content) { return /Content-Security-Policy/i.test(content); } validateXFrameOptions(content) { return /X-Frame-Options/i.test(content); } validateXContentTypeOptions(content) { return /X-Content-Type-Options/i.test(content); } validateXXSSProtection(content) { return /X-XSS-Protection/i.test(content); } validateHSTS(content) { return /Strict-Transport-Security/i.test(content); } validateReferrerPolicy(content) { return /Referrer-Policy/i.test(content); } validatePermissionsPolicy(content) { return /Permissions-Policy/i.test(content); } validateXPermittedCrossDomainPolicies(content) { return /X-Permitted-Cross-Domain-Policies/i.test(content); } } exports.MissingSecurityHeadersRule = MissingSecurityHeadersRule; //# sourceMappingURL=missing-security-headers.js.map