mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
fix(core): prevent infinite recursion in symlink resolution (#21750)
This commit is contained in:
@@ -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', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user