mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 23:41:29 -07:00
fix(plan): sandbox path resolution in Plan Mode to prevent hallucinations (#22737)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -35,19 +35,13 @@ describe('planUtils', () => {
|
||||
const fullPath = path.join(tempRootDir, planPath);
|
||||
fs.writeFileSync(fullPath, '# My Plan');
|
||||
|
||||
const result = await validatePlanPath(planPath, plansDir, tempRootDir);
|
||||
const result = await validatePlanPath(planPath, plansDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return error for path traversal', async () => {
|
||||
const planPath = path.join('..', 'secret.txt');
|
||||
const result = await validatePlanPath(planPath, plansDir, tempRootDir);
|
||||
expect(result).toContain('Access denied');
|
||||
});
|
||||
|
||||
it('should return error for non-existent file', async () => {
|
||||
const planPath = path.join('plans', 'ghost.md');
|
||||
const result = await validatePlanPath(planPath, plansDir, tempRootDir);
|
||||
const result = await validatePlanPath(planPath, plansDir);
|
||||
expect(result).toContain('Plan file does not exist');
|
||||
});
|
||||
|
||||
@@ -60,11 +54,7 @@ describe('planUtils', () => {
|
||||
// Create a symbolic link pointing outside the plans directory
|
||||
fs.symlinkSync(outsideFile, fullMaliciousPath);
|
||||
|
||||
const result = await validatePlanPath(
|
||||
maliciousPath,
|
||||
plansDir,
|
||||
tempRootDir,
|
||||
);
|
||||
const result = await validatePlanPath(maliciousPath, plansDir);
|
||||
expect(result).toContain('Access denied');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ import { isSubpath, resolveToRealPath } from './paths.js';
|
||||
* Shared between backend tools and CLI UI for consistency.
|
||||
*/
|
||||
export const PlanErrorMessages = {
|
||||
PATH_ACCESS_DENIED:
|
||||
'Access denied: plan path must be within the designated plans directory.',
|
||||
PATH_ACCESS_DENIED: (planPath: string, plansDir: string) =>
|
||||
`Access denied: plan path (${planPath}) must be within the designated plans directory (${plansDir}).`,
|
||||
FILE_NOT_FOUND: (path: string) =>
|
||||
`Plan file does not exist: ${path}. You must create the plan file before requesting approval.`,
|
||||
FILE_EMPTY:
|
||||
@@ -32,14 +32,14 @@ export const PlanErrorMessages = {
|
||||
export async function validatePlanPath(
|
||||
planPath: string,
|
||||
plansDir: string,
|
||||
targetDir: string,
|
||||
): Promise<string | null> {
|
||||
const resolvedPath = path.resolve(targetDir, planPath);
|
||||
const safeFilename = path.basename(planPath);
|
||||
const resolvedPath = path.join(plansDir, safeFilename);
|
||||
const realPath = resolveToRealPath(resolvedPath);
|
||||
const realPlansDir = resolveToRealPath(plansDir);
|
||||
|
||||
if (!isSubpath(realPlansDir, realPath)) {
|
||||
return PlanErrorMessages.PATH_ACCESS_DENIED;
|
||||
return PlanErrorMessages.PATH_ACCESS_DENIED(planPath, realPlansDir);
|
||||
}
|
||||
|
||||
if (!(await fileExists(resolvedPath))) {
|
||||
|
||||
Reference in New Issue
Block a user