Security hardening: path validation, input clamping, safe JSON, file locking
Implements security improvements inspired by PR #16 (credit: MO2k4): - Add validatePathWithinRoot() to prevent path traversal attacks in extraction and context building - Clamp MCP tool inputs (limit, depth, maxDepth) to sane ranges - Use atomic writes (temp file + rename) for config saves - Add symlink cycle detection in directory scanning to prevent infinite loops - Replace all JSON.parse calls in db/queries.ts with safeJsonParse fallbacks to handle corrupted database metadata gracefully - Add cross-process FileLock for DB write operations (indexAll, indexFiles, sync) to prevent concurrent writes from CLI, MCP server, and git hooks - Remove unused path import from context/index.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
38fac1ff28
commit
932c567d18
+59
-1
@@ -19,6 +19,7 @@ import { extractFromSource } from './tree-sitter';
|
||||
import { detectLanguage, isLanguageSupported } from './grammars';
|
||||
import { logDebug } from '../errors';
|
||||
import { captureException } from '../sentry';
|
||||
import { validatePathWithinRoot } from '../utils';
|
||||
|
||||
/**
|
||||
* Progress callback for indexing operations
|
||||
@@ -127,8 +128,22 @@ export function scanDirectory(
|
||||
): string[] {
|
||||
const files: string[] = [];
|
||||
let count = 0;
|
||||
const visitedRealPaths = new Set<string>(); // Symlink cycle detection
|
||||
|
||||
function walk(dir: string): void {
|
||||
// Symlink cycle detection: resolve real path and skip if already visited
|
||||
try {
|
||||
const realDir = fs.realpathSync(dir);
|
||||
if (visitedRealPaths.has(realDir)) {
|
||||
logDebug('Skipping directory to prevent symlink cycle', { dir, realDir });
|
||||
return;
|
||||
}
|
||||
visitedRealPaths.add(realDir);
|
||||
} catch {
|
||||
// If realpath fails, skip this directory
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for .codegraphignore marker file - skip entire directory tree if present
|
||||
const ignoreMarker = path.join(dir, CODEGRAPH_IGNORE_MARKER);
|
||||
if (fs.existsSync(ignoreMarker)) {
|
||||
@@ -149,6 +164,39 @@ export function scanDirectory(
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relativePath = path.relative(rootDir, fullPath);
|
||||
|
||||
// Follow symlinked directories, but skip symlinked files to non-project targets
|
||||
if (entry.isSymbolicLink()) {
|
||||
try {
|
||||
const realTarget = fs.realpathSync(fullPath);
|
||||
const stat = fs.statSync(realTarget);
|
||||
if (stat.isDirectory()) {
|
||||
// Check exclusion, then recurse (cycle detection handles the rest)
|
||||
const dirPattern = relativePath + '/';
|
||||
let excluded = false;
|
||||
for (const pattern of config.exclude) {
|
||||
if (matchesGlob(dirPattern, pattern) || matchesGlob(relativePath, pattern)) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!excluded) {
|
||||
walk(fullPath);
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (shouldIncludeFile(relativePath, config)) {
|
||||
files.push(relativePath);
|
||||
count++;
|
||||
if (onProgress) {
|
||||
onProgress(count, relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logDebug('Skipping broken symlink', { path: fullPath });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Check if directory should be excluded
|
||||
const dirPattern = relativePath + '/';
|
||||
@@ -335,7 +383,17 @@ export class ExtractionOrchestrator {
|
||||
* Index a single file
|
||||
*/
|
||||
async indexFile(relativePath: string): Promise<ExtractionResult> {
|
||||
const fullPath = path.join(this.rootDir, relativePath);
|
||||
const fullPath = validatePathWithinRoot(this.rootDir, relativePath);
|
||||
|
||||
if (!fullPath) {
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
unresolvedReferences: [],
|
||||
errors: [{ message: `Path traversal blocked: ${relativePath}`, severity: 'error' }],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Check file exists and is readable
|
||||
let content: string;
|
||||
|
||||
Reference in New Issue
Block a user