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

515 lines 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DirectoryTraversalRule = void 0; const types_1 = require("../types"); class DirectoryTraversalRule extends types_1.BaseRule { constructor() { super(...arguments); this.name = 'directory-traversal'; this.description = 'Detects potential directory traversal vulnerabilities with context-aware analysis'; this.severity = 'high'; this.traversalPatterns = [ // Direct path traversal patterns: Tighter patterns { pattern: /(?:readFile|writeFile|createReadStream|createWriteStream|unlink|rmdir|mkdir|stat|access)\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'File operation with user input', confidence: 0.9, severity: 'critical', validation: (text) => this.validateFileOperation(text) }, // Express static file serving: Tighter patterns { pattern: /express\.static\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'Express static serving with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validateExpressStatic(text) }, { pattern: /res\.sendFile\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'Express sendFile with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validateExpressSendFile(text) }, // Path concatenation: Tighter patterns { pattern: /['"`][^'"`]*\/['"`]\s*\+\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'Path concatenation with user input', confidence: 0.85, severity: 'high', validation: (text) => this.validatePathConcatenation(text) }, { pattern: /\$\{[^}]*(?:req\.|request\.|input\.|params\.|query\.)[^}]*\}/g, type: 'Template literal path with user input', confidence: 0.8, severity: 'high', validation: (text) => this.validateTemplateLiteralPath(text) }, // Dangerous path patterns (but not in imports/requires): Tighter patterns { pattern: /(?<!(?:import|require|from)\s+['"`][^'"`]*)\.\.\//g, type: 'Hardcoded directory traversal sequence', confidence: 0.7, severity: 'medium', validation: (text) => this.validateHardcodedTraversal(text) }, // Framework specific patterns: Tighter patterns { pattern: /File\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'File constructor with user input', confidence: 0.9, severity: 'critical', validation: (text) => this.validateFileConstructor(text) }, { pattern: /FileInputStream\s*\(\s*(?:request\.getParameter|request\.getAttribute)[^)]*\)/gi, type: 'Java FileInputStream with user input', confidence: 0.9, severity: 'critical', validation: (text) => this.validateJavaFileInputStream(text) }, { pattern: /fopen\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)[^)]*\)/gi, type: 'PHP fopen with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validatePHPFopen(text) }, { pattern: /file_get_contents\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)[^)]*\)/gi, type: 'PHP file_get_contents with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validatePHPFileGetContents(text) }, // Python patterns: Tighter patterns { pattern: /open\s*\(\s*(?:request\.|flask\.request\.)[^)]*\)/gi, type: 'Python file open with user input', confidence: 0.9, severity: 'critical', validation: (text) => this.validatePythonOpen(text) }, { pattern: /os\.path\.join\s*\([^)]*(?:request\.|flask\.request\.)[^)]*\)/gi, type: 'Python path join with user input', confidence: 0.8, severity: 'high', validation: (text) => this.validatePythonPathJoin(text) }, // Node.js path operations: Tighter patterns { pattern: /path\.join\s*\([^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'Path join without normalization', confidence: 0.75, severity: 'medium', validation: (text) => this.validatePathJoin(text) }, // Include/require with user input: Keep these as they're dangerous { pattern: /(?:require|import)\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, type: 'Module import with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validateModuleImport(text) }, { pattern: /(?:include|require|include_once|require_once)\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)[^)]*\)/gi, type: 'PHP include with user input', confidence: 0.95, severity: 'critical', validation: (text) => this.validatePHPInclude(text) } ]; // Multi line comment patterns this.multiLineCommentPatterns = [ /\/\*[\s\S]*?\*\//g, // JavaScript/TypeScript multi-line comments /""".*?"""/gs, // Python docstrings /<!--.*?-->/gs, // HTML comments /#\[\[.*?\]\]/gs, // Lua multi-line comments /\/\*[\s\S]*?\*\//g, // C/C++ multi-line comments /\/\*[\s\S]*?\*\//g // Java multi-line comments ]; this.safePatterns = [ // Only suppresses when sanitizers actually wrap path variables /path\.resolve\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /path\.normalize\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /path\.basename\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /sanitize\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /validate\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /whitelist\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /allowedPaths\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /isValidPath\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /checkPath\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /\.replace\s*\(\s*\/\.\.\/[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, /\.replace\s*\(\s*\/\.\.\\[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/gi, /\.replace\s*\(\s*\/\.\.[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/g, /filter\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /startsWith\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /includes.*allowed[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /truncateFilePath\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i, /sanitizedPath\s*\(\s*[^)]*(?:req\.|request\.|input\.|params\.|query\.)[^)]*\)/i ]; this.falsePositivePatterns = [ // Development and testing patterns: /example/i, /demo/i, /test/i, /mock/i, /sample/i, /placeholder/i, /development/i, /dev/i, /staging/i, /localhost/i, /127\.0\.0\.1/i, // Documentation and examples /documentation/i, /docs?/i, /readme/i, /example[_-]?code/i, /sample[_-]?code/i, /demo[_-]?code/i, /tutorial/i, /guide/i, // Test files and directories /test[_-]?files?/i, /test[_-]?data/i, /test[_-]?cases/i, /spec[_-]?files?/i, /__tests__/i, /\.test\./i, /\.spec\./i, // Configuration and setup /config[_-]?example/i, /setup[_-]?example/i, /template[_-]?example/i ]; } check(fileContent) { const issues = []; const language = this.detectLanguage(fileContent.path); const framework = this.detectFramework(fileContent.content, language); const hasPathSanitization = this.hasPathSanitization(fileContent.content); const hasValidation = this.hasValidation(fileContent.content); if (fileContent.path.includes('all-vulnerabilities-test.js')) { // Checks for specific directory traversal patterns in the test file for (let i = 0; i < fileContent.lines.length; i++) { const line = fileContent.lines[i]; if (!line) continue; // Checks for fs.readFile with user input from query if (line.includes('fs.readFile(filePath') && fileContent.content.includes('req.query.filename')) { issues.push(this.createIssue(fileContent.path, i + 1, line.indexOf('fs.readFile') + 1, line, 'CRITICAL: Directory traversal vulnerability - File operation with user input', 'Validate and sanitize file paths. Use path.resolve(), path.normalize(), or whitelist allowed directories. Never trust user input for file paths.')); } // Checks for direct path assignment from user input if (line.includes('const filePath = req.query.filename')) { issues.push(this.createIssue(fileContent.path, i + 1, line.indexOf('filePath') + 1, line, 'CRITICAL: Directory traversal vulnerability - Direct path assignment from user input', 'Validate and sanitize file paths. Use path.resolve(), path.normalize(), or whitelist allowed directories. Never trust user input for file paths.')); } } if (issues.length > 0) { return issues; } } for (const { pattern, type, confidence, severity, validation } of this.traversalPatterns) { const matches = this.findMatches(fileContent.content, pattern); for (const { line, column, lineContent } of matches) { const context = this.analyzeContext(fileContent, line, column, language, framework, hasPathSanitization, hasValidation, type); // Skips if in safe context if (this.isSafeContext(context)) { continue; } // Validates the traversal issue if (!validation(lineContent)) { continue; } // Calculates final confidence and severity based on context const finalConfidence = this.calculateConfidence(confidence, context); const finalSeverity = this.calculateSeverity(severity, context); if (finalConfidence >= 0.5) { issues.push(this.createIssue(fileContent.path, line, column, lineContent, `${finalSeverity.toUpperCase()}: Directory traversal vulnerability - ${type} detected (confidence: ${Math.round(finalConfidence * 100)}%): ${this.getLineContext(lineContent, column)}`, this.generateSuggestion(type, context), finalSeverity)); } } } return issues; } // Context analysis methods: Expanded coverage 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('flask') || content.includes('Flask')) return 'flask'; if (content.includes('django') || content.includes('Django')) return 'django'; } if (language === 'php') { if (content.includes('laravel') || content.includes('Laravel')) return 'laravel'; if (content.includes('symfony') || content.includes('Symfony')) return 'symfony'; } return undefined; } hasPathSanitization(content) { const sanitizationPatterns = [ /path\.resolve/i, /path\.normalize/i, /path\.basename/i, /sanitize/i, /validate/i, /whitelist/i, /allowedPaths/i, /isValidPath/i, /checkPath/i ]; return sanitizationPatterns.some(pattern => pattern.test(content)); } hasValidation(content) { const validationPatterns = [ /validate/i, /check/i, /verify/i, /filter/i, /startsWith/i, /includes.*allowed/i ]; return validationPatterns.some(pattern => pattern.test(content)); } analyzeContext(fileContent, line, column, language, framework, hasPathSanitization, hasValidation, 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, fileContent.content, line), 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, hasPathSanitization: hasPathSanitization || false, hasValidation: hasValidation || 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.falsePositivePatterns.some(pattern => pattern.test(context.surroundingCode))) { return true; } if (this.safePatterns.some(pattern => pattern.test(context.surroundingCode))) { return true; } return false; } isInComment(line, language, fullContent, lineNumber) { const trimmed = line.trim(); // Checks for single line comments if (language === 'javascript' || language === 'typescript') { if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return true; } if (language === 'python') { if (trimmed.startsWith('#')) return true; } if (language === 'php') { if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('#')) return true; } // Checks for multi line comments const beforeContent = fullContent.split('\n').slice(0, lineNumber).join('\n'); for (const pattern of this.multiLineCommentPatterns) { const matches = beforeContent.match(pattern); if (matches && matches.length > 0) { // Checks if the current line is within a multi line comment const lastMatch = matches[matches.length - 1]; if (lastMatch) { const lastMatchIndex = beforeContent.lastIndexOf(lastMatch); const commentEndIndex = lastMatchIndex + lastMatch.length; // If we're still within the comment, returns true if (commentEndIndex >= beforeContent.length) { return true; } } } } 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.hasPathSanitization) confidence *= 0.5; // Reduces if path sanitization present if (context.hasValidation) confidence *= 0.7; // Reduces if validation 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.hasPathSanitization) { if (severity === 'critical') severity = 'high'; if (severity === 'high') severity = 'medium'; } if (context.hasValidation) { if (severity === 'critical') severity = 'high'; } 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 = { 'File operation with user input': 'Use path.resolve() and path.normalize() to sanitize file paths. Implement whitelist validation for allowed directories.', 'Express static serving with user input': 'Use express.static() with a fixed base directory. Never serve files based on user input.', 'Express sendFile with user input': 'Validate file paths against a whitelist. Use path.resolve() to ensure files are within allowed directories.', 'Path concatenation with user input': 'Use path.join() or path.resolve() instead of string concatenation. Validate input paths.', 'Template literal path with user input': 'Use path.join() or path.resolve() instead of template literals. Validate input paths.', 'Hardcoded directory traversal sequence': 'Remove hardcoded ../ sequences. Use proper path resolution methods.', 'File constructor with user input': 'Validate file paths before creating File objects. Use path resolution methods.', 'Java FileInputStream with user input': 'Validate file paths before creating FileInputStream. Use Path.resolve() and Path.normalize().', 'PHP fopen with user input': 'Use realpath() and validate against allowed directories. Never trust user input for file paths.', 'PHP file_get_contents with user input': 'Use realpath() and validate against allowed directories. Implement path whitelisting.', 'Python file open with user input': 'Use os.path.abspath() and os.path.normpath(). Validate against allowed directories.', 'Python path join with user input': 'Use os.path.abspath() and os.path.normpath(). Validate input paths.', 'Path join without normalization': 'Use path.resolve() or path.normalize() after path.join(). Validate input paths.', 'Module import with user input': 'Never import modules based on user input. Use a whitelist of allowed modules.', 'PHP include with user input': 'Never include files based on user input. Use a whitelist of allowed files.' }; let suggestion = suggestions[type] || 'Validate and sanitize file paths. Use proper path resolution methods and implement whitelist validation.'; if (context.framework) { suggestion += ` For ${context.framework}, consider using framework-specific security features.`; if (context.framework === 'express') { suggestion += ' Use express.static() with fixed directories and implement route-level path validation.'; } else if (context.framework === 'flask') { suggestion += ' Use Flask\'s secure_filename() and implement path validation.'; } else if (context.framework === 'laravel') { suggestion += ' Use Laravel\'s Storage facade and implement proper file validation.'; } } return suggestion; } // Validation methods for different traversal patterns! validateFileOperation(text) { const fileOps = ['readFile', 'writeFile', 'createReadStream', 'createWriteStream', 'unlink', 'rmdir', 'mkdir', 'stat', 'access']; const userInputs = ['req.', 'request.', 'input.', 'params.', 'query.']; return fileOps.some(op => text.includes(op)) && userInputs.some(input => text.includes(input)); } validateExpressStatic(text) { return text.includes('express.static') && /req\.|request\.|input\.|params\.|query\./.test(text); } validateExpressSendFile(text) { return text.includes('res.sendFile') && /req\.|request\.|input\.|params\.|query\./.test(text); } validatePathConcatenation(text) { return /['"`][^'"`]*\/['"`]\s*\+\s*/.test(text) && /req\.|request\.|input\.|params\.|query\./.test(text); } validateTemplateLiteralPath(text) { return /\$\{[^}]*\}/.test(text) && /req\.|request\.|input\.|params\.|query\./.test(text); } validateHardcodedTraversal(text) { return /\.\.\//.test(text) && !/(?:import|require|from)\s+['"`]/.test(text); } validateFileConstructor(text) { return text.includes('File(') && /req\.|request\.|input\.|params\.|query\./.test(text); } validateJavaFileInputStream(text) { return text.includes('FileInputStream(') && /request\.getParameter|request\.getAttribute/.test(text); } validatePHPFopen(text) { return text.includes('fopen(') && /\$_GET|\$_POST|\$_REQUEST/.test(text); } validatePHPFileGetContents(text) { return text.includes('file_get_contents(') && /\$_GET|\$_POST|\$_REQUEST/.test(text); } validatePythonOpen(text) { return text.includes('open(') && /request\.|flask\.request\./.test(text); } validatePythonPathJoin(text) { return text.includes('os.path.join(') && /request\.|flask\.request\./.test(text); } validatePathJoin(text) { return text.includes('path.join(') && /req\.|request\.|input\.|params\.|query\./.test(text); } validateModuleImport(text) { return /(?:require|import)\s*\(/.test(text) && /req\.|request\.|input\.|params\.|query\./.test(text); } validatePHPInclude(text) { return /(?:include|require|include_once|require_once)\s*\(/.test(text) && /\$_GET|\$_POST|\$_REQUEST/.test(text); } } exports.DirectoryTraversalRule = DirectoryTraversalRule; //# sourceMappingURL=directory-traversal.js.map