mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-03 22:56:48 -07:00
fix(core): resolve symbolic link directory escape in memory import processor (#28233)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as fsSync from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { marked } from 'marked';
|
||||
import { processImports, validateImportPath } from './memoryImportProcessor.js';
|
||||
@@ -867,5 +869,46 @@ describe('memoryImportProcessor', () => {
|
||||
);
|
||||
expect(validateImportPath(dotPath, basePath, [allowedPath])).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject paths that escape allowed directories via symbolic links', () => {
|
||||
const tmpDir = fsSync.realpathSync(os.tmpdir());
|
||||
const testRoot = fsSync.mkdtempSync(path.join(tmpDir, 'gemini-test-'));
|
||||
const allowedDir = path.join(testRoot, 'allowed');
|
||||
const outsideDir = path.join(testRoot, 'outside');
|
||||
const symlinkDir = path.join(allowedDir, 'sym_outside');
|
||||
|
||||
try {
|
||||
// Create real directories and files on disk
|
||||
fsSync.mkdirSync(allowedDir, { recursive: true });
|
||||
fsSync.mkdirSync(outsideDir, { recursive: true });
|
||||
fsSync.writeFileSync(path.join(outsideDir, 'sensitive.md'), 'secret');
|
||||
|
||||
// Create a symbolic link pointing outside the allowed directory
|
||||
try {
|
||||
fsSync.symlinkSync(outsideDir, symlinkDir, 'dir');
|
||||
} catch (err: unknown) {
|
||||
if (
|
||||
process.platform === 'win32' &&
|
||||
err &&
|
||||
typeof err === 'object' &&
|
||||
'code' in err &&
|
||||
err.code === 'EPERM'
|
||||
) {
|
||||
// Skip the test if the user lacks symlink creation privileges on Windows
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const importPath = 'sym_outside/sensitive.md';
|
||||
|
||||
expect(validateImportPath(importPath, allowedDir, [allowedDir])).toBe(
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
// Cleanup
|
||||
fsSync.rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { isSubpath } from './paths.js';
|
||||
import { isSubpath, resolveToRealPath } from './paths.js';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
|
||||
// Simple console logger for import processing
|
||||
@@ -397,9 +397,28 @@ export function validateImportPath(
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(basePath, importPath);
|
||||
let resolvedPath: string;
|
||||
try {
|
||||
// Canonicalize the path on the actual physical disk to resolve symlinks
|
||||
resolvedPath = resolveToRealPath(path.resolve(basePath, importPath));
|
||||
} catch {
|
||||
// If path resolution fails (e.g., infinite recursion or invalid path), fail-closed and reject it
|
||||
return false;
|
||||
}
|
||||
|
||||
return allowedDirectories.some((allowedDir) =>
|
||||
isSubpath(allowedDir, resolvedPath),
|
||||
const realAllowedDirs = allowedDirectories
|
||||
.map((dir) => {
|
||||
const trimmed = dir.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return resolveToRealPath(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((dir): dir is string => dir !== null);
|
||||
|
||||
return realAllowedDirs.some((realAllowedDir) =>
|
||||
isSubpath(realAllowedDir, resolvedPath),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user