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:
Colby McHenry
2026-02-09 23:18:40 -06:00
co-authored by Claude Opus 4.6
parent 38fac1ff28
commit 932c567d18
7 changed files with 257 additions and 55 deletions
+6 -5
View File
@@ -8,6 +8,7 @@ import CodeGraph, { findNearestCodeGraphRoot } from '../index';
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
import { createHash } from 'crypto';
import { writeFileSync } from 'fs';
import { clamp } from '../utils';
import { tmpdir } from 'os';
import { join } from 'path';
@@ -351,7 +352,7 @@ export class ToolHandler {
const cg = this.getCodeGraph(args.projectPath as string | undefined);
const query = args.query as string;
const kind = args.kind as string | undefined;
const limit = (args.limit as number) || 10;
const limit = clamp((args.limit as number) || 10, 1, 100);
const results = cg.searchNodes(query, {
limit,
@@ -436,7 +437,7 @@ export class ToolHandler {
private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> {
const cg = this.getCodeGraph(args.projectPath as string | undefined);
const symbol = args.symbol as string;
const limit = (args.limit as number) || 20;
const limit = clamp((args.limit as number) || 20, 1, 100);
// First find the node by name
const results = cg.searchNodes(symbol, { limit: 1 });
@@ -463,7 +464,7 @@ export class ToolHandler {
private async handleCallees(args: Record<string, unknown>): Promise<ToolResult> {
const cg = this.getCodeGraph(args.projectPath as string | undefined);
const symbol = args.symbol as string;
const limit = (args.limit as number) || 20;
const limit = clamp((args.limit as number) || 20, 1, 100);
// First find the node by name
const results = cg.searchNodes(symbol, { limit: 1 });
@@ -490,7 +491,7 @@ export class ToolHandler {
private async handleImpact(args: Record<string, unknown>): Promise<ToolResult> {
const cg = this.getCodeGraph(args.projectPath as string | undefined);
const symbol = args.symbol as string;
const depth = (args.depth as number) || 2;
const depth = clamp((args.depth as number) || 2, 1, 10);
// First find the node by name
const results = cg.searchNodes(symbol, { limit: 1 });
@@ -574,7 +575,7 @@ export class ToolHandler {
const pattern = args.pattern as string | undefined;
const format = (args.format as 'tree' | 'flat' | 'grouped') || 'tree';
const includeMetadata = args.includeMetadata !== false;
const maxDepth = args.maxDepth as number | undefined;
const maxDepth = args.maxDepth != null ? clamp(args.maxDepth as number, 1, 20) : undefined;
// Get all files from the index
const allFiles = cg.getFiles();