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