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
JavaScript
;
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