fix(core): resolve symbolic link directory escape in memory import processor (#28233)

This commit is contained in:
luisfelipe-alt
2026-07-01 19:23:32 +00:00
committed by GitHub
parent 7f00c5fe59
commit ff00dacd9f
2 changed files with 66 additions and 4 deletions
@@ -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),
);
}