diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 227afaf44a..4563c0485b 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -484,6 +484,10 @@ describe('shortenPath', () => { }); describe('resolveToRealPath', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it.each([ { description: @@ -542,6 +546,28 @@ describe('resolveToRealPath', () => { expect(resolveToRealPath(childPath)).toBe(expectedPath); }); + + it('should prevent infinite recursion on malicious symlink structures', () => { + const maliciousPath = path.resolve('malicious', 'symlink'); + + vi.spyOn(fs, 'realpathSync').mockImplementation(() => { + const err = new Error('ENOENT') as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; + }); + + vi.spyOn(fs, 'lstatSync').mockImplementation( + () => ({ isSymbolicLink: () => true }) as fs.Stats, + ); + + vi.spyOn(fs, 'readlinkSync').mockImplementation(() => + ['..', 'malicious', 'symlink'].join(path.sep), + ); + + expect(() => resolveToRealPath(maliciousPath)).toThrow( + /Infinite recursion detected/, + ); + }); }); describe('normalizePath', () => { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index aa167e3558..338d4017e5 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -375,7 +375,12 @@ export function resolveToRealPath(pathStr: string): string { return robustRealpath(path.resolve(resolvedPath)); } -function robustRealpath(p: string): string { +function robustRealpath(p: string, visited = new Set()): string { + const key = process.platform === 'win32' ? p.toLowerCase() : p; + if (visited.has(key)) { + throw new Error(`Infinite recursion detected in robustRealpath: ${p}`); + } + visited.add(key); try { return fs.realpathSync(p); } catch (e: unknown) { @@ -385,14 +390,25 @@ function robustRealpath(p: string): string { if (stat.isSymbolicLink()) { const target = fs.readlinkSync(p); const resolvedTarget = path.resolve(path.dirname(p), target); - return robustRealpath(resolvedTarget); + return robustRealpath(resolvedTarget, visited); + } + } catch (lstatError: unknown) { + // Not a symlink, or lstat failed. Re-throw if it's not an expected + // ENOENT (e.g., a permissions error), otherwise resolve parent. + if ( + !( + lstatError && + typeof lstatError === 'object' && + 'code' in lstatError && + lstatError.code === 'ENOENT' + ) + ) { + throw lstatError; } - } catch { - // Not a symlink, or lstat failed. Just resolve parent. } const parent = path.dirname(p); if (parent === p) return p; - return path.join(robustRealpath(parent), path.basename(p)); + return path.join(robustRealpath(parent, visited), path.basename(p)); } throw e; }