@@ -3,8 +3,19 @@ import fs from 'node:fs';
33import path from 'node:path' ;
44import { findCycles } from './cycles.js' ;
55import { findDbPath , openReadonlyOrFail } from './db.js' ;
6+ import { debug } from './logger.js' ;
67import { 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+
819const TEST_PATTERN = / \. ( t e s t | s p e c ) \. | _ _ t e s t _ _ | _ _ t e s t s _ _ | \. s t o r i e s \. / ;
920function isTestFile ( filePath ) {
1021 return TEST_PATTERN . test ( filePath ) ;
@@ -1132,13 +1143,15 @@ export function fnDeps(name, customDbPath, opts = {}) {
11321143
11331144function 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