Skip to content

Commit 93a9bcf

Browse files
fix: add path traversal guard and debug logging to file read helpers
Add safePath() helper that validates resolved paths stay within repoRoot, preventing directory traversal. Replace bare catch blocks with debug logging for easier troubleshooting in verbose mode.
1 parent 2ac24ef commit 93a9bcf

1 file changed

Lines changed: 29 additions & 6 deletions

File tree

src/queries.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@ import fs from 'node:fs';
33
import path from 'node:path';
44
import { findCycles } from './cycles.js';
55
import { findDbPath, openReadonlyOrFail } from './db.js';
6+
import { debug } from './logger.js';
67
import { LANGUAGE_REGISTRY } from './parser.js';
78

9+
/**
10+
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
11+
* Returns null if the resolved path escapes repoRoot.
12+
*/
13+
function safePath(repoRoot, file) {
14+
const resolved = path.resolve(repoRoot, file);
15+
if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) return null;
16+
return resolved;
17+
}
18+
819
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
920
function isTestFile(filePath) {
1021
return TEST_PATTERN.test(filePath);
@@ -1132,13 +1143,15 @@ export function fnDeps(name, customDbPath, opts = {}) {
11321143

11331144
function readSourceRange(repoRoot, file, startLine, endLine) {
11341145
try {
1135-
const absPath = path.resolve(repoRoot, file);
1146+
const absPath = safePath(repoRoot, file);
1147+
if (!absPath) return null;
11361148
const content = fs.readFileSync(absPath, 'utf-8');
11371149
const lines = content.split('\n');
11381150
const start = Math.max(0, (startLine || 1) - 1);
11391151
const end = Math.min(lines.length, endLine || startLine + 50);
11401152
return lines.slice(start, end).join('\n');
1141-
} catch {
1153+
} catch (e) {
1154+
debug(`readSourceRange failed for ${file}: ${e.message}`);
11421155
return null;
11431156
}
11441157
}
@@ -1262,11 +1275,16 @@ export function contextData(name, customDbPath, opts = {}) {
12621275
function getFileLines(file) {
12631276
if (fileCache.has(file)) return fileCache.get(file);
12641277
try {
1265-
const absPath = path.resolve(repoRoot, file);
1278+
const absPath = safePath(repoRoot, file);
1279+
if (!absPath) {
1280+
fileCache.set(file, null);
1281+
return null;
1282+
}
12661283
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
12671284
fileCache.set(file, lines);
12681285
return lines;
1269-
} catch {
1286+
} catch (e) {
1287+
debug(`getFileLines failed for ${file}: ${e.message}`);
12701288
fileCache.set(file, null);
12711289
return null;
12721290
}
@@ -1703,11 +1721,16 @@ export function explainData(target, customDbPath, opts = {}) {
17031721
function getFileLines(file) {
17041722
if (fileCache.has(file)) return fileCache.get(file);
17051723
try {
1706-
const absPath = path.resolve(repoRoot, file);
1724+
const absPath = safePath(repoRoot, file);
1725+
if (!absPath) {
1726+
fileCache.set(file, null);
1727+
return null;
1728+
}
17071729
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
17081730
fileCache.set(file, lines);
17091731
return lines;
1710-
} catch {
1732+
} catch (e) {
1733+
debug(`getFileLines failed for ${file}: ${e.message}`);
17111734
fileCache.set(file, null);
17121735
return null;
17131736
}

0 commit comments

Comments
 (0)