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:
@@ -115,6 +115,20 @@ export function removeDirectory(projectRoot: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify .codegraph is a real directory, not a symlink pointing elsewhere
|
||||
const lstat = fs.lstatSync(codegraphDir);
|
||||
if (lstat.isSymbolicLink()) {
|
||||
// Only remove the symlink itself, never follow it for recursive delete
|
||||
fs.unlinkSync(codegraphDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lstat.isDirectory()) {
|
||||
// Not a directory - remove the single file
|
||||
fs.unlinkSync(codegraphDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recursively remove directory
|
||||
fs.rmSync(codegraphDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -137,6 +151,11 @@ export function listDirectoryContents(projectRoot: string): string[] {
|
||||
for (const entry of entries) {
|
||||
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
|
||||
// Skip symlinks to prevent following links outside .codegraph
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(path.join(dir, entry.name), relativePath);
|
||||
} else {
|
||||
@@ -165,6 +184,11 @@ export function getDirectorySize(projectRoot: string): number {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip symlinks to prevent following links outside .codegraph
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
@@ -184,6 +208,10 @@ export function getDirectorySize(projectRoot: string): number {
|
||||
* Ensure a subdirectory exists within .codegraph
|
||||
*/
|
||||
export function ensureSubdirectory(projectRoot: string, subdirName: string): string {
|
||||
if (subdirName.includes('..') || subdirName.includes(path.sep) || subdirName.includes('/')) {
|
||||
throw new Error(`Invalid subdirectory name: ${subdirName}`);
|
||||
}
|
||||
|
||||
const subdirPath = path.join(getCodeGraphDir(projectRoot), subdirName);
|
||||
|
||||
if (!fs.existsSync(subdirPath)) {
|
||||
|
||||
Reference in New Issue
Block a user