diff --git a/packages/core/src/utils/memoryImportProcessor.test.ts b/packages/core/src/utils/memoryImportProcessor.test.ts index 3c9a74b604..78d584f0b2 100644 --- a/packages/core/src/utils/memoryImportProcessor.test.ts +++ b/packages/core/src/utils/memoryImportProcessor.test.ts @@ -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 }); + } + }); }); }); diff --git a/packages/core/src/utils/memoryImportProcessor.ts b/packages/core/src/utils/memoryImportProcessor.ts index dc4b0b8537..cd4b9aa111 100644 --- a/packages/core/src/utils/memoryImportProcessor.ts +++ b/packages/core/src/utils/memoryImportProcessor.ts @@ -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), ); }