security: path validation, ReDoS prevention, picomatch, PID-based file lock
- Add validateProjectPath() to reject sensitive system directories - Add isPathWithinRoot/isPathWithinRootReal for symlink-aware path checks - Replace hand-rolled glob-to-regex with picomatch to prevent ReDoS - Add isSafeRegex() to reject custom patterns with nested quantifiers - Replace FileLock with PID-tracking version that detects stale locks - Add symlink detection in removeDirectory/listDirectoryContents - Add subdirectory name validation in ensureSubdirectory - Add atomicWriteFileSync and corrupted file backup in config-writer - Add MCP input validation (validateString) for all tool handlers - Fix CLAUDE.md section replacement to handle ### subsections correctly
This commit is contained in:
+31
-9
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import picomatch from 'picomatch';
|
||||
import { CodeGraphConfig, DEFAULT_CONFIG, Language, NodeKind } from './types';
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,31 @@ export function getConfigPath(projectRoot: string): string {
|
||||
return path.join(projectRoot, '.codegraph', CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a regex pattern is safe from ReDoS attacks.
|
||||
*
|
||||
* Rejects patterns with nested quantifiers (e.g., (a+)+, (a*)*) which
|
||||
* are the primary source of catastrophic backtracking. Also rejects
|
||||
* excessively long patterns and validates compilability.
|
||||
*/
|
||||
function isSafeRegex(pattern: string): boolean {
|
||||
// Reject excessively long patterns
|
||||
if (pattern.length > 500) return false;
|
||||
|
||||
// Reject nested quantifiers: (...)+ followed by +, *, or {
|
||||
// These are the primary cause of catastrophic backtracking
|
||||
if (/([+*}])\s*[+*{]/.test(pattern)) return false;
|
||||
if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) return false;
|
||||
|
||||
// Verify the pattern is a valid regex
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a configuration object
|
||||
*/
|
||||
@@ -75,6 +101,9 @@ export function validateConfig(config: unknown): config is CodeGraphConfig {
|
||||
if (typeof p.name !== 'string') return false;
|
||||
if (typeof p.pattern !== 'string') return false;
|
||||
if (typeof p.kind !== 'string') return false;
|
||||
|
||||
// Validate regex is compilable and reject patterns with known ReDoS risks
|
||||
if (!isSafeRegex(p.pattern)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,15 +272,8 @@ export function shouldIncludeFile(filePath: string, config: CodeGraphConfig): bo
|
||||
// Simple glob matching (for now, just check if any pattern matches)
|
||||
// A full implementation would use a proper glob library
|
||||
|
||||
const matchesPattern = (pattern: string, path: string): boolean => {
|
||||
// Convert glob to regex (simplified)
|
||||
const regexStr = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*\*/g, '.*')
|
||||
.replace(/\*/g, '[^/]*')
|
||||
.replace(/\?/g, '.');
|
||||
const regex = new RegExp(`^${regexStr}$`);
|
||||
return regex.test(path);
|
||||
const matchesPattern = (pattern: string, filePath: string): boolean => {
|
||||
return picomatch.isMatch(filePath, pattern, { dot: true });
|
||||
};
|
||||
|
||||
// Check exclude patterns first
|
||||
|
||||
Reference in New Issue
Block a user