fix(core): prevent infinite recursion in symlink resolution (#21750)

This commit is contained in:
Adib234
2026-03-09 15:38:45 -04:00
committed by GitHub
parent 527074b50a
commit 4f4431e4e1
2 changed files with 47 additions and 5 deletions

View File

@@ -484,6 +484,10 @@ describe('shortenPath', () => {
}); });
describe('resolveToRealPath', () => { describe('resolveToRealPath', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it.each([ it.each([
{ {
description: description:
@@ -542,6 +546,28 @@ describe('resolveToRealPath', () => {
expect(resolveToRealPath(childPath)).toBe(expectedPath); 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', () => { describe('normalizePath', () => {

View File

@@ -375,7 +375,12 @@ export function resolveToRealPath(pathStr: string): string {
return robustRealpath(path.resolve(resolvedPath)); return robustRealpath(path.resolve(resolvedPath));
} }
function robustRealpath(p: string): string { function robustRealpath(p: string, visited = new Set<string>()): 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 { try {
return fs.realpathSync(p); return fs.realpathSync(p);
} catch (e: unknown) { } catch (e: unknown) {
@@ -385,14 +390,25 @@ function robustRealpath(p: string): string {
if (stat.isSymbolicLink()) { if (stat.isSymbolicLink()) {
const target = fs.readlinkSync(p); const target = fs.readlinkSync(p);
const resolvedTarget = path.resolve(path.dirname(p), target); 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); const parent = path.dirname(p);
if (parent === p) return 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; throw e;
} }