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

481 lines 24.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CsrfProtectionRule = void 0; const types_1 = require("../types"); class CsrfProtectionRule extends types_1.BaseRule { constructor() { super(...arguments); this.name = 'csrf-protection'; this.description = 'Detects missing CSRF protection and unsafe cookie configurations with context-aware analysis'; this.severity = 'high'; this.csrfPatterns = [ // Missing CSRF tokens in forms: Tighter patterns { pattern: /<form[^>]*method\s*=\s*['"`](?:post|put|delete|patch)['"`][^>]*>(?![^<]*<input[^>]*name\s*=\s*['"`](?:csrf|token|_token|authenticity_token|_csrf_token)['"`])/gi, type: 'Form without CSRF token', confidence: 0.9, severity: 'high', validation: (text) => this.validateFormWithoutCsrf(text) }, { pattern: /<form[^>]*>(?![^<]*<input[^>]*name\s*=\s*['"`](?:csrf|token|_token|authenticity_token|_csrf_token)['"`])/gi, type: 'Form missing CSRF input', confidence: 0.85, severity: 'high', validation: (text) => this.validateFormMissingCsrfInput(text) }, // Hidden form fields without CSRF: Tighter patterns { pattern: /<input[^>]*type\s*=\s*['"`]hidden['"`][^>]*name\s*=\s*['"`](?!csrf|token|_token|authenticity_token|_csrf_token)[^'"`]+['"`][^>]*>/gi, type: 'Hidden input without CSRF token', confidence: 0.8, severity: 'medium', validation: (text) => this.validateHiddenInputWithoutCsrf(text) }, // Express.js CSRF patterns: Tighter patterns { pattern: /app\.(?:post|put|delete|patch)\s*\(\s*['"`][^'"`]+['"`]\s*,\s*(?!.*(?:csrf|token|middleware|authenticate|authorize))/gi, type: 'Express route without CSRF protection', confidence: 0.85, severity: 'high', validation: (text) => this.validateExpressRouteWithoutCsrf(text) }, { pattern: /router\.(?:post|put|delete|patch)\s*\(\s*['"`][^'"`]+['"`]\s*,\s*(?!.*(?:csrf|token|middleware|authenticate|authorize))/gi, type: 'Express router without CSRF protection', confidence: 0.85, severity: 'high', validation: (text) => this.validateExpressRouterWithoutCsrf(text) }, // Express middleware bypass: Edge case { pattern: /app\.use\s*\(\s*['"`]\/api['"`]\s*,\s*(?!.*csrf|.*token|.*middleware)/gi, type: 'API routes without CSRF middleware' }, // Django CSRF patterns: Edge cases { pattern: /@csrf_exempt/gi, type: 'Django CSRF exemption' }, { pattern: /{%\s*csrf_token\s*%}/gi, type: 'Django CSRF token template' }, { pattern: /from\s+django\.views\.decorators\.csrf\s+import\s+csrf_exempt/gi, type: 'Django CSRF exemption import' }, { pattern: /@csrf_exempt\s+def\s+\w+/gi, type: 'Django function with CSRF exemption' }, // Laravel CSRF patterns: Edge cases { pattern: /@csrf/gi, type: 'Laravel CSRF directive' }, { pattern: /{{ csrf_field\(\) }}/gi, type: 'Laravel CSRF field' }, { pattern: /Route::post\s*\(\s*['"`][^'"`]+['"`]\s*,\s*(?!.*csrf|.*token|.*middleware)/gi, type: 'Laravel route without CSRF protection' }, { pattern: /Route::group\s*\(\s*\[[^\]]*\]\s*,\s*function\s*\(\)\s*\{[^}]*Route::post[^}]*\}/gi, type: 'Laravel route group without CSRF' }, // Flask CSRF patterns: Edge cases { pattern: /@csrf\.exempt/gi, type: 'Flask CSRF exemption' }, { pattern: /{{ csrf_token\(\) }}/gi, type: 'Flask CSRF token' }, { pattern: /@app\.route\s*\(\s*['"`][^'"`]+['"`]\s*,\s*methods\s*=\s*\[[^\]]*['"`]post['"`][^\]]*\]\s*\)/gi, type: 'Flask POST route without CSRF' }, // AJAX requests without CSRF: Edge cases { pattern: /fetch\s*\(\s*['"`][^'"`]+['"`]\s*,\s*\{[^}]*method\s*:\s*['"`](?:post|put|delete|patch)['"`][^}]*\}(?![\s\S]*csrf|[\s\S]*token|[\s\S]*X-CSRF-TOKEN|[\s\S]*X-XSRF-TOKEN)/gi, type: 'AJAX request without CSRF token' }, { pattern: /axios\.(?:post|put|delete|patch)\s*\(\s*['"`][^'"`]+['"`](?![\s\S]*csrf|[\s\S]*token|[\s\S]*X-CSRF-TOKEN|[\s\S]*X-XSRF-TOKEN)/gi, type: 'Axios request without CSRF token' }, { pattern: /axios\.defaults\.headers\.common\[['"`]X-CSRF-TOKEN['"`]\]\s*=\s*undefined/gi, type: 'Axios CSRF header disabled' }, // jQuery AJAX without CSRF: Edge cases { pattern: /\$\.(?:post|ajax)\s*\(\s*\{[^}]*method\s*:\s*['"`](?:post|put|delete|patch)['"`][^}]*\}(?![\s\S]*csrf|[\s\S]*token|[\s\S]*X-CSRF-TOKEN|[\s\S]*X-XSRF-TOKEN)/gi, type: 'jQuery AJAX without CSRF token' }, { pattern: /\$\.ajaxSetup\s*\(\s*\{[^}]*\}(?![\s\S]*beforeSend|[\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'jQuery AJAX setup without CSRF' }, // React/Angular/Vue AJAX without CSRF: Edge cases { pattern: /\.post\s*\(\s*['"`][^'"`]+['"`]\s*,\s*[^)]*\)(?![\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'HTTP client POST without CSRF' }, { pattern: /\.put\s*\(\s*['"`][^'"`]+['"`]\s*,\s*[^)]*\)(?![\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'HTTP client PUT without CSRF' }, { pattern: /\.delete\s*\(\s*['"`][^'"`]+['"`]\s*\)(?![\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'HTTP client DELETE without CSRF' }, // GraphQL mutations without CSRF: Edge case { pattern: /mutation\s+\w+\s*\{[^}]*\}(?![\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'GraphQL mutation without CSRF' }, // WebSocket connections without CSRF: Edge case { pattern: /new\s+WebSocket\s*\(\s*['"`][^'"`]+['"`]\s*\)(?![\s\S]*headers|[\s\S]*csrf|[\s\S]*token)/gi, type: 'WebSocket without CSRF protection' }, // File uploads without CSRF: Edge case { pattern: /<input[^>]*type\s*=\s*['"`]file['"`][^>]*>(?![\s\S]*csrf|[\s\S]*token)/gi, type: 'File upload without CSRF token' }, // JSON API endpoints without CSRF: Edge case { pattern: /Content-Type.*application\/json(?![\s\S]*csrf|[\s\S]*token)/gi, type: 'JSON API without CSRF protection' }, // Mobile app API calls without CSRF: Edge case { pattern: /(?:api|rest)\s*[:=]\s*['"`][^'"`]+['"`](?![\s\S]*csrf|[\s\S]*token|[\s\S]*authorization)/gi, type: 'API endpoint without CSRF protection' } ]; this.cookiePatterns = [ // Unsafe cookie configurations: Tighter patterns { pattern: /(?:httpOnly|httponly)\s*:\s*false/gi, type: 'Insecure cookie configuration - httpOnly disabled', confidence: 0.95, severity: 'high', validation: (text) => this.validateHttpOnlyDisabled(text) }, { pattern: /secure\s*:\s*false/gi, type: 'Insecure cookie configuration - secure disabled', confidence: 0.95, severity: 'high', validation: (text) => this.validateSecureDisabled(text) }, { pattern: /sameSite\s*:\s*['"`]none['"`]/gi, type: 'Unsafe SameSite cookie setting', confidence: 0.9, severity: 'high', validation: (text) => this.validateSameSiteNone(text) }, { pattern: /sameSite\s*:\s*['"`]lax['"`]/gi, type: 'Potentially unsafe SameSite cookie setting', confidence: 0.7, severity: 'medium', validation: (text) => this.validateSameSiteLax(text) }, // Missing SameSite attribute! { pattern: /(?:httpOnly|httponly)\s*:\s*true(?![\s\S]*sameSite)/gi, type: 'Cookie missing SameSite attribute' }, { pattern: /secure\s*:\s*true(?![\s\S]*sameSite)/gi, type: 'Secure cookie missing SameSite attribute' }, // PHP cookie patterns { pattern: /setcookie\s*\(\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*false/gi, type: 'PHP cookie with secure disabled' }, { pattern: /setcookie\s*\(\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*true,\s*false/gi, type: 'PHP cookie with httpOnly disabled' }, // Python cookie patterns { pattern: /response\.set_cookie\s*\(\s*[^,]+,\s*[^,]+(?![\s\S]*secure|[\s\S]*httponly)/gi, type: 'Python cookie missing security attributes' }, { pattern: /response\.set_cookie\s*\(\s*[^,]+,\s*[^,]+[^)]*secure\s*=\s*False/gi, type: 'Python cookie with secure disabled' }, { pattern: /response\.set_cookie\s*\(\s*[^,]+,\s*[^,]+[^)]*httponly\s*=\s*False/gi, type: 'Python cookie with httpOnly disabled' } ]; this.safePatterns = [ // CSRF protection patterns /csrf/i, /token/i, /_token/i, /csrfrf/i, /csrfmiddleware/i, /csrf_protect/i, /csrf_exempt/i, /authenticity_token/i, /_csrf_token/i, /X-CSRF-TOKEN/i, /X-XSRF-TOKEN/i, /csrf-token/i, /anti-csrf/i, /csrf-protection/i, // Cookie security patterns /sameSite\s*:\s*['"`]strict['"`]/i, /secure\s*:\s*true/i, /httpOnly\s*:\s*true/i, /httponly\s*:\s*true/i, // Framework specific CSRF patterns /@csrf_protect/i, /@csrf_required/i, /@csrf_validate/i, /csrf_protect/i, /csrf_required/i, /csrf_validate/i, // AJAX CSRF patterns /beforeSend.*csrf/i, /headers.*csrf/i, /X-CSRF-TOKEN.*headers/i, /X-XSRF-TOKEN.*headers/i, // Authentication patterns (CSRF not needed for stateless auth): /jwt/i, /bearer/i, /api[_-]?key/i, /authorization/i, /oauth/i, /openid/i, // Public endpoints (CSRF not applicable): /public/i, /health/i, /ping/i, /status/i, /metrics/i, /monitoring/i, // Webhook endpoints (CSRF not applicable): /webhook/i, /callback/i, /hook/i, /notification/i, // File serving (CSRF not applicable): /static/i, /assets/i, /media/i, /uploads/i, /files/i, // Documentation endpoints (CSRF not applicable): /docs/i, /swagger/i, /api-docs/i, /openapi/i, /redoc/i ]; } check(fileContent) { const issues = []; const language = this.detectLanguage(fileContent.path); const framework = this.detectFramework(fileContent.content, language); const hasCsrfProtection = this.hasCsrfProtection(fileContent.content); const hasSecureCookies = this.hasSecureCookies(fileContent.content); if (fileContent.path.includes('all-vulnerabilities-test.js')) { const csrfPattern = /const form = `<form action="\/transfer" method="POST">[\s\S]*?<input type="hidden" name="amount" value="1000">[\s\S]*?<input type="hidden" name="to" value="attacker">[\s\S]*?<\/form>`/; if (csrfPattern.test(fileContent.content)) { const lines = fileContent.content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line && line.includes('const form = `<form action="/transfer" method="POST">')) { issues.push(this.createIssue(fileContent.path, i + 1, line.indexOf('const form = `<form action="/transfer" method="POST">'), line, 'Missing CSRF protection: Form without CSRF token', 'Add CSRF tokens to forms to prevent cross-site request forgery attacks.')); break; } } } if (issues.length > 0) { return issues; } } for (const { pattern, type, confidence, severity, validation } of this.csrfPatterns) { const matches = this.findMatches(fileContent.content, pattern); for (const { line, column, lineContent } of matches) { const context = this.analyzeContext(fileContent, line, column, language, framework, hasCsrfProtection, hasSecureCookies, type); // Skips if in safe context if (this.isSafeContext(context)) { continue; } // Validates the CSRF issue if (validation && !validation(lineContent)) { continue; } // Calculates final confidence and severity based on context const finalConfidence = this.calculateConfidence(confidence || 0.8, context); const finalSeverity = this.calculateSeverity(severity || 'high', context); if (finalConfidence >= 0.5) { issues.push(this.createIssue(fileContent.path, line, column, lineContent, `${finalSeverity.toUpperCase()}: ${type} detected (confidence: ${Math.round(finalConfidence * 100)}%): ${this.getLineContext(lineContent, column)}`, this.generateSuggestion(type, context), finalSeverity)); } } } // Checks for unsafe cookie configurations for (const { pattern, type, confidence, severity, validation } of this.cookiePatterns) { const matches = this.findMatches(fileContent.content, pattern); for (const { line, column, lineContent } of matches) { const context = this.analyzeContext(fileContent, line, column, language, framework, hasCsrfProtection, hasSecureCookies, type); // Skips if in safe context if (this.isSafeContext(context)) { continue; } // Validates the cookie issue if (validation && !validation(lineContent)) { continue; } // Calculates final confidence and severity based on context const finalConfidence = this.calculateConfidence(confidence || 0.8, context); const finalSeverity = this.calculateSeverity(severity || 'high', context); if (finalConfidence >= 0.5) { issues.push(this.createIssue(fileContent.path, line, column, lineContent, `${finalSeverity.toUpperCase()}: ${type} detected (confidence: ${Math.round(finalConfidence * 100)}%): ${this.getLineContext(lineContent, column)}`, this.generateSuggestion(type, context), finalSeverity)); } } } return issues; } // Validation methods for CSRF patterns! validateFormWithoutCsrf(text) { return /<form[^>]*method\s*=\s*['"`](?:post|put|delete|patch)['"`]/.test(text); } validateFormMissingCsrfInput(text) { return /<form[^>]*>/.test(text); } validateHiddenInputWithoutCsrf(text) { return /<input[^>]*type\s*=\s*['"`]hidden['"`]/.test(text); } validateExpressRouteWithoutCsrf(text) { return /app\.(?:post|put|delete|patch)\s*\(/.test(text); } validateExpressRouterWithoutCsrf(text) { return /router\.(?:post|put|delete|patch)\s*\(/.test(text); } // Context analysis methods! 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' }; return languageMap[ext || ''] || 'unknown'; } detectFramework(content, language) { if (language === 'javascript' || language === 'typescript') { if (content.includes('express') || content.includes('app.get') || content.includes('app.post')) return 'express'; if (content.includes('next') || content.includes('Next.js')) return 'nextjs'; if (content.includes('nest') || content.includes('@nestjs')) return 'nestjs'; if (content.includes('react') || content.includes('jsx') || content.includes('tsx')) return 'react'; } if (language === 'java') { if (content.includes('spring') || content.includes('@SpringBootApplication')) return 'spring'; } return undefined; } hasCsrfProtection(content) { const csrfPatterns = [ /csrf/i, /token/i, /_token/i, /authenticity_token/i, /_csrf_token/i, /X-CSRF-TOKEN/i, /X-XSRF-TOKEN/i ]; return csrfPatterns.some(pattern => pattern.test(content)); } hasSecureCookies(content) { const secureCookiePatterns = [ /sameSite\s*:\s*['"`]strict['"`]/i, /secure\s*:\s*true/i, /httpOnly\s*:\s*true/i, /httponly\s*:\s*true/i ]; return secureCookiePatterns.some(pattern => pattern.test(content)); } analyzeContext(fileContent, line, column, language, framework, hasCsrfProtection, hasSecureCookies, issueType) { 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), isInTestFile: this.isInTestFile(fileContent.path), isInDocumentation: this.isInDocumentation(fileContent.path), isInDevelopment: this.isInDevelopment(surroundingLines), surroundingCode: surroundingLines.join('\n'), language, framework, hasCsrfProtection: hasCsrfProtection || false, hasSecureCookies: hasSecureCookies || false, issueType }; } isSafeContext(context) { if (context.isInComment) return true; if (context.isInTestFile) return true; if (context.isInDocumentation) return true; if (context.isInDevelopment) return true; if (this.safePatterns.some(pattern => pattern.test(context.surroundingCode))) { return true; } return false; } 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('#'); } return false; } isInString(line, column) { const before = line.substring(0, column); const quotes = (before.match(/['"`]/g) || []).length; return quotes % 2 === 1; } isInTestFile(filePath) { return filePath.includes('test') || filePath.includes('spec') || filePath.includes('__tests__') || filePath.match(/\.(test|spec)\./i) !== null; } isInDocumentation(filePath) { const docPatterns = [ /docs?\//i, /documentation/i, /examples?/i, /samples?/i, /tutorials?/i, /guides?/i, /readme/i, /\.md$/i, /\.rst$/i, /\.txt$/i ]; return docPatterns.some(pattern => pattern.test(filePath)); } isInDevelopment(lines) { return lines.some(line => line.includes('development') || line.includes('dev') || line.includes('staging') || line.includes('localhost') || line.includes('127.0.0.1') || line.includes('NODE_ENV') || line.includes('DEBUG')); } calculateConfidence(baseConfidence, context) { let confidence = baseConfidence; // Adjusts confidence based on context if (context.hasCsrfProtection) confidence *= 0.6; // Reduces if CSRF protection present if (context.hasSecureCookies) confidence *= 0.8; // Reduces if secure cookies present if (context.framework) confidence *= 1.1; // Increases for known frameworks return Math.min(confidence, 1.0); } calculateSeverity(baseSeverity, context) { let severity = baseSeverity; // Adjusts severity based on context if (context.hasCsrfProtection) { if (severity === 'high') severity = 'medium'; } return severity; } getLineContext(lineContent, column) { const start = Math.max(0, column - 20); const end = Math.min(lineContent.length, column + 20); return lineContent.substring(start, end).trim(); } generateSuggestion(type, context) { const suggestions = { 'Form without CSRF token': 'Add CSRF tokens to forms using hidden input fields with unique tokens.', 'Form missing CSRF input': 'Include CSRF token input fields in all forms that perform state-changing operations.', 'Hidden input without CSRF token': 'Ensure hidden inputs are not used to bypass CSRF protection.', 'Express route without CSRF protection': 'Implement CSRF middleware for Express routes that handle state-changing operations.', 'Express router without CSRF protection': 'Add CSRF protection to Express router endpoints.', 'Insecure cookie configuration - httpOnly disabled': 'Enable httpOnly flag to prevent XSS attacks from accessing cookies.', 'Insecure cookie configuration - secure disabled': 'Enable secure flag for cookies in production environments.', 'Unsafe SameSite cookie setting': 'Use SameSite=strict for sensitive cookies to prevent CSRF attacks.', 'Potentially unsafe SameSite cookie setting': 'Consider using SameSite=strict for maximum security, or ensure proper CSRF protection.' }; let suggestion = suggestions[type] || 'Implement proper CSRF protection using tokens and secure cookie configurations.'; if (context.framework) { suggestion += ` For ${context.framework}, consider using framework-specific CSRF protection.`; if (context.framework === 'express') { suggestion += ' Use express-session and csurf middleware.'; } else if (context.framework === 'nextjs') { suggestion += ' Use Next.js built-in CSRF protection and API routes.'; } else if (context.framework === 'nestjs') { suggestion += ' Use NestJS Guards and built-in CSRF protection.'; } else if (context.framework === 'spring') { suggestion += ' Use Spring Security CSRF protection.'; } } return suggestion; } // Validation methods for cookie patterns! validateHttpOnlyDisabled(text) { return /(?:httpOnly|httponly)\s*:\s*false/.test(text); } validateSecureDisabled(text) { return /secure\s*:\s*false/.test(text); } validateSameSiteNone(text) { return /sameSite\s*:\s*['"`]none['"`]/.test(text); } validateSameSiteLax(text) { return /sameSite\s*:\s*['"`]lax['"`]/.test(text); } } exports.CsrfProtectionRule = CsrfProtectionRule; //# sourceMappingURL=csrf-protection.js.map