From 7c0cfd53dfb70e610e82456fdce9df5961b29d82 Mon Sep 17 00:00:00 2001 From: Mahima Shanware Date: Tue, 7 Apr 2026 05:12:30 +0000 Subject: [PATCH] fix(core): gracefully degrade global config dir path validation on EACCES If the user's home directory or global `.gemini` config directory has restrictive permissions that prevent `realpathSync` from succeeding, the CLI should not crash. Instead, it now gracefully degrades by omitting the global config directory from the valid security boundaries for plan directories, allowing the CLI to continue operating securely within the project root. --- packages/core/src/config/config.ts | 18 ++++++++++-------- packages/core/src/tools/tool-registry.test.ts | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 780717beca..8372a6619d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -2271,18 +2271,20 @@ export class Config implements McpContext, AgentLoopContext { } const realProjectRoot = this.storage.getRealProjectRoot(); - let realGlobalGeminiDir: string; + let realGlobalGeminiDir: string | undefined; try { realGlobalGeminiDir = resolveToRealPath(Storage.getGlobalGeminiDir()); } catch { - realGlobalGeminiDir = path.resolve(Storage.getGlobalGeminiDir()); + // If we can't securely resolve the global config dir (e.g. strict EACCES permissions on ~/), + // we gracefully degrade by not allowing it as a valid security boundary for plans. + realGlobalGeminiDir = undefined; } // 1. Lexical security check (before any filesystem mutation or return) const lexicalPlansDir = path.resolve(plansDir); if ( !isSubpath(realProjectRoot, lexicalPlansDir) && - !isSubpath(realGlobalGeminiDir, lexicalPlansDir) + (!realGlobalGeminiDir || !isSubpath(realGlobalGeminiDir, lexicalPlansDir)) ) { throw new SecurityError( `Security violation: Plan directory '${lexicalPlansDir}' is outside both the project root '${realProjectRoot}' and the global configuration directory.`, @@ -2302,15 +2304,15 @@ export class Config implements McpContext, AgentLoopContext { let realPlansDir: string; try { realPlansDir = resolveToRealPath(plansDir); - } catch { - // Fallback to path.resolve if the directory doesn't exist yet (e.g. mkdirSync failed) - // so that the security check can still be performed on the absolute path. - realPlansDir = path.resolve(plansDir); + } catch (e: unknown) { + throw new SecurityError( + `Security violation: Could not securely resolve plan directory '${plansDir}'. System error: ${e instanceof Error ? e.message : String(e)}`, + ); } if ( !isSubpath(realProjectRoot, realPlansDir) && - !isSubpath(realGlobalGeminiDir, realPlansDir) + (!realGlobalGeminiDir || !isSubpath(realGlobalGeminiDir, realPlansDir)) ) { throw new SecurityError( `Security violation: Resolved plan directory '${realPlansDir}' is outside both the project root '${realProjectRoot}' and the global configuration directory.`, diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index 006bfcd894..9e5e317840 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -237,6 +237,7 @@ describe('ToolRegistry', () => { beforeEach(() => { vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString()); vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true, } as fs.Stats);