UNPKG

vibe-guard

Version:

🛡️ Vibe-Guard Security Scanner - Catch security issues before they catch you!

184 lines 9.56 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'; this.severity = 'high'; this.traversalPatterns = [ // Direct path traversal patterns { pattern: /(?:readFile|writeFile|createReadStream|createWriteStream|unlink|rmdir|mkdir|stat|access)\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)[^)]*(?!\s*(?:path\.resolve|path\.join|path\.normalize))/gi, type: 'File operation with user input' }, // Express static file serving { pattern: /express\.static\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'Express static serving with user input' }, { pattern: /res\.sendFile\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'Express sendFile with user input' }, // Path concatenation { pattern: /['"`][^'"`]*\/['"`]\s*\+\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'Path concatenation with user input' }, { pattern: /\$\{[^}]*(?:req\.|request\.|input\.|params\.|query\.)[^}]*\}/g, type: 'Template literal path with user input' }, // Dangerous path patterns (but not in imports/requires) { pattern: /(?<!(?:import|require|from)\s+['"`][^'"`]*)\.\.\//g, type: 'Hardcoded directory traversal sequence' }, // Framework-specific patterns { pattern: /File\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'File constructor with user input' }, { pattern: /FileInputStream\s*\(\s*(?:request\.getParameter|request\.getAttribute)/gi, type: 'Java FileInputStream with user input' }, { pattern: /fopen\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)/gi, type: 'PHP fopen with user input' }, { pattern: /file_get_contents\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)/gi, type: 'PHP file_get_contents with user input' }, // Python patterns { pattern: /open\s*\(\s*(?:request\.|flask\.request\.)/gi, type: 'Python file open with user input' }, { pattern: /os\.path\.join\s*\([^)]*(?:request\.|flask\.request\.)/gi, type: 'Python path join with user input' }, // Node.js path operations { pattern: /path\.join\s*\([^)]*(?:req\.|request\.|input\.|params\.|query\.)(?![^)]*(?:path\.resolve|path\.normalize))/gi, type: 'Path join without normalization' }, // Include/require with user input { pattern: /(?:require|import)\s*\(\s*(?:req\.|request\.|input\.|params\.|query\.)/gi, type: 'Module import with user input' }, { pattern: /(?:include|require|include_once|require_once)\s*\(\s*(?:\$_GET|\$_POST|\$_REQUEST)/gi, type: 'PHP include with user input' } ]; this.safePatterns = [ /path\.resolve/i, /path\.normalize/i, /path\.basename/i, /sanitize/i, /validate/i, /whitelist/i, /allowedPaths/i, /isValidPath/i, /checkPath/i, /\.replace\s*\(\s*\/\.\.\//gi, /\.replace\s*\(\s*\/\.\.\\/gi, /\.replace\s*\(\s*\/\.\./g, /filter/i, /startsWith/i, /includes.*allowed/i, /truncateFilePath/i, /sanitizedPath/i, /replace\s*\(\s*\/\.\./g, /replace\s*\(\s*\/\.\.\//g, /replace\s*\(\s*\/\.\.\\/g ]; } check(fileContent) { const issues = []; const { content, lines, path } = fileContent; // If the file is a test file, skip all checks const testFilePatterns = [ /test/i, /spec/i, /\.test\./i, /\.spec\./i, /__tests__/i, /tests\//i, /spec\//i ]; if (testFilePatterns.some(pattern => pattern.test(path))) { return issues; } // 1. Detect direct user input in file operations lines.forEach((lineContent, idx) => { if (/fs\.(readFile|writeFile|createReadStream|createWriteStream)\s*\(\s*filePath/.test(lineContent)) { // Check if filePath is assigned from user input above for (let i = Math.max(0, idx - 3); i <= idx; i++) { if (lines[i] && /const\s+filePath\s*=\s*req\.query\./.test(lines[i])) { issues.push(this.createIssue(path, idx + 1, (lineContent.indexOf('fs.') || 0) + 1, lineContent, 'Potential 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.')); break; } } } }); // 2. Detect path concatenation vulnerabilities lines.forEach((lineContent, idx) => { if (/const\s+filePath\s*=\s*basePath\s*\+\s*req\.query\.filename/.test(lineContent)) { issues.push(this.createIssue(path, idx + 1, (lineContent.indexOf('basePath') || 0) + 1, lineContent, 'Path concatenation vulnerability: Path concatenation with user input', 'Use path.resolve() or path.join() and validate input.')); } }); // 3. Detect template literal path vulnerabilities lines.forEach((lineContent, idx) => { if (/const\s+filePath\s*=.*\$\{basePath\}\$\{req\.query\.filename\}/.test(lineContent)) { issues.push(this.createIssue(path, idx + 1, (lineContent.indexOf('basePath') || 0) + 1, lineContent, 'Template literal path vulnerability: Template literal path with user input', 'Use path.resolve() or path.join() and validate input.')); } }); // Fallback to original traversalPatterns for other cases for (const { pattern, type } of this.traversalPatterns) { const matches = this.findMatches(content, pattern); for (const { line, column, lineContent } of matches) { if (this.hasSafePathHandling(content, line)) continue; if (this.isCommentOrTest(lineContent, path)) continue; if (this.isImportStatement(lineContent)) continue; if (type === 'Hardcoded directory traversal sequence' && this.isTestContext(content, line)) continue; issues.push(this.createIssue(path, line, column, lineContent, `Potential directory traversal vulnerability: ${type}`, 'Validate and sanitize file paths. Use path.resolve(), path.normalize(), or whitelist allowed directories. Never trust user input for file paths.')); } } return issues; } hasSafePathHandling(content, lineNumber) { const lines = content.split('\n'); const contextRange = 5; // Check 5 lines before and after const startLine = Math.max(0, lineNumber - contextRange - 1); const endLine = Math.min(lines.length, lineNumber + contextRange); const contextLines = lines.slice(startLine, endLine).join('\n'); return this.safePatterns.some(pattern => pattern.test(contextLines)); } isCommentOrTest(line, filePath) { // Check if line is a comment const commentPatterns = [ /^\s*\/\//, // JavaScript comment /^\s*#/, // Python/Shell comment /^\s*\*/ // Multi-line comment ]; if (commentPatterns.some(pattern => pattern.test(line))) { return true; } // Check if it's a test file const testPatterns = [ /test/i, /spec/i, /\.test\./i, /\.spec\./i, /__tests__/i, /tests\//i, /spec\//i ]; return testPatterns.some(pattern => pattern.test(filePath)); } isImportStatement(line) { const importPatterns = [ /^\s*import\s+.*from\s+['"`]/, /^\s*import\s+['"`]/, /^\s*const\s+.*=\s+require\s*\(\s*['"`]/, /^\s*let\s+.*=\s+require\s*\(\s*['"`]/, /^\s*var\s+.*=\s+require\s*\(\s*['"`]/, /^\s*export\s+.*from\s+['"`]/, /^\s*from\s+['"`]/ ]; return importPatterns.some(pattern => pattern.test(line)); } isTestContext(content, lineNumber) { const lines = content.split('\n'); const contextRange = 10; const startLine = Math.max(0, lineNumber - contextRange - 1); const endLine = Math.min(lines.length, lineNumber + contextRange); const contextLines = lines.slice(startLine, endLine).join('\n'); const testPatterns = [ /test/i, /spec/i, /describe/i, /it\(/i, /expect/i, /assert/i, /mock/i, /example/i, /demo/i, /truncateFilePath/i, /sanitizedPath/i, /replace\s*\(\s*\/\.\./g, /replace\s*\(\s*\/\.\.\//g, /replace\s*\(\s*\/\.\.\\/g ]; return testPatterns.some(pattern => pattern.test(contextLines)); } } exports.DirectoryTraversalRule = DirectoryTraversalRule; //# sourceMappingURL=directory-traversal.js.map