mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix(trust): Refuse to load from untrusted process.cwd() sources; Add tests (#7323)
This commit is contained in:
@@ -34,7 +34,7 @@ vi.mock('./trustedFolders.js', () => ({
|
||||
}));
|
||||
|
||||
// NOW import everything else, including the (now effectively re-exported) settings.js
|
||||
import * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH
|
||||
import path, * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
@@ -58,7 +58,9 @@ import {
|
||||
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
|
||||
migrateSettingsToV1,
|
||||
type Settings,
|
||||
loadEnvironment,
|
||||
} from './settings.js';
|
||||
import { GEMINI_DIR } from '@google/gemini-cli-core';
|
||||
|
||||
const MOCK_WORKSPACE_DIR = '/mock/workspace';
|
||||
// Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency
|
||||
@@ -2363,4 +2365,54 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadEnvironment', () => {
|
||||
function setup({
|
||||
isFolderTrustEnabled = true,
|
||||
isWorkspaceTrustedValue = true,
|
||||
}) {
|
||||
delete process.env['TESTTEST']; // reset
|
||||
const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env'));
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(isWorkspaceTrustedValue);
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()),
|
||||
);
|
||||
const userSettingsContent: Settings = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: isFolderTrustEnabled,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
fileName: 'USER_CONTEXT.md',
|
||||
},
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === geminiEnvPath) return 'TESTTEST=1234';
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
it('sets environment variables from .env files', () => {
|
||||
setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true });
|
||||
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
|
||||
|
||||
expect(process.env['TESTTEST']).toEqual('1234');
|
||||
});
|
||||
|
||||
it('does not load env files from untrusted spaces', () => {
|
||||
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
|
||||
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
|
||||
|
||||
expect(process.env['TESTTEST']).not.toEqual('1234');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -556,6 +556,10 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
|
||||
export function loadEnvironment(settings: Settings): void {
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
|
||||
if (!isWorkspaceTrusted(settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cloud Shell environment variable handling
|
||||
if (process.env['CLOUD_SHELL'] === 'true') {
|
||||
setUpCloudShellEnvironment(envFilePath);
|
||||
|
||||
@@ -80,6 +80,52 @@ describe('Trusted Folders Loading', () => {
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
describe('isPathTrusted', () => {
|
||||
function setup({ config = {} as Record<string, TrustLevel> } = {}) {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p) => p === USER_TRUSTED_FOLDERS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation((p) => {
|
||||
if (p === USER_TRUSTED_FOLDERS_PATH) return JSON.stringify(config);
|
||||
return '{}';
|
||||
});
|
||||
|
||||
const folders = loadTrustedFolders();
|
||||
|
||||
return { folders };
|
||||
}
|
||||
|
||||
it('provides a method to determine if a path is trusted', () => {
|
||||
const { folders } = setup({
|
||||
config: {
|
||||
'./myfolder': TrustLevel.TRUST_FOLDER,
|
||||
'/trustedparent/trustme': TrustLevel.TRUST_PARENT,
|
||||
'/user/folder': TrustLevel.TRUST_FOLDER,
|
||||
'/secret': TrustLevel.DO_NOT_TRUST,
|
||||
'/secret/publickeys': TrustLevel.TRUST_FOLDER,
|
||||
},
|
||||
});
|
||||
expect(folders.isPathTrusted('/secret')).toBe(false);
|
||||
expect(folders.isPathTrusted('/user/folder')).toBe(true);
|
||||
expect(folders.isPathTrusted('/secret/publickeys/public.pem')).toBe(true);
|
||||
expect(folders.isPathTrusted('/user/folder/harhar')).toBe(true);
|
||||
expect(folders.isPathTrusted('myfolder/somefile.jpg')).toBe(true);
|
||||
expect(folders.isPathTrusted('/trustedparent/someotherfolder')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(folders.isPathTrusted('/trustedparent/trustme')).toBe(true);
|
||||
|
||||
// No explicit rule covers this file
|
||||
expect(folders.isPathTrusted('/secret/bankaccounts.json')).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(folders.isPathTrusted('/secret/mine/privatekey.pem')).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(folders.isPathTrusted('/user/someotherfolder')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('should load user rules if only user file exists', () => {
|
||||
const userPath = USER_TRUSTED_FOLDERS_PATH;
|
||||
(mockFsExistsSync as Mock).mockImplementation((p) => p === userPath);
|
||||
|
||||
@@ -42,8 +42,8 @@ export interface TrustedFoldersFile {
|
||||
|
||||
export class LoadedTrustedFolders {
|
||||
constructor(
|
||||
public user: TrustedFoldersFile,
|
||||
public errors: TrustedFoldersError[],
|
||||
readonly user: TrustedFoldersFile,
|
||||
readonly errors: TrustedFoldersError[],
|
||||
) {}
|
||||
|
||||
get rules(): TrustRule[] {
|
||||
@@ -53,6 +53,49 @@ export class LoadedTrustedFolders {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true or false if the path should be "trusted". This function
|
||||
* should only be invoked when the folder trust setting is active.
|
||||
*
|
||||
* @param location path
|
||||
* @returns
|
||||
*/
|
||||
isPathTrusted(location: string): boolean | undefined {
|
||||
const trustedPaths: string[] = [];
|
||||
const untrustedPaths: string[] = [];
|
||||
|
||||
for (const rule of this.rules) {
|
||||
switch (rule.trustLevel) {
|
||||
case TrustLevel.TRUST_FOLDER:
|
||||
trustedPaths.push(rule.path);
|
||||
break;
|
||||
case TrustLevel.TRUST_PARENT:
|
||||
trustedPaths.push(path.dirname(rule.path));
|
||||
break;
|
||||
case TrustLevel.DO_NOT_TRUST:
|
||||
untrustedPaths.push(rule.path);
|
||||
break;
|
||||
default:
|
||||
// Do nothing for unknown trust levels.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const trustedPath of trustedPaths) {
|
||||
if (isWithinRoot(location, trustedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const untrustedPath of untrustedPaths) {
|
||||
if (path.normalize(location) === path.normalize(untrustedPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setValue(path: string, trustLevel: TrustLevel): void {
|
||||
this.user.config[path] = trustLevel;
|
||||
saveTrustedFolders(this.user);
|
||||
@@ -110,59 +153,28 @@ export function saveTrustedFolders(
|
||||
}
|
||||
}
|
||||
|
||||
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
|
||||
/** Is folder trust feature enabled per the current applied settings */
|
||||
export function isFolderTrustEnabled(settings: Settings): boolean {
|
||||
const folderTrustFeature =
|
||||
settings.security?.folderTrust?.featureEnabled ?? false;
|
||||
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
|
||||
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
|
||||
return folderTrustFeature && folderTrustSetting;
|
||||
}
|
||||
|
||||
if (!folderTrustEnabled) {
|
||||
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
|
||||
if (!isFolderTrustEnabled(settings)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { rules, errors } = loadTrustedFolders();
|
||||
const folders = loadTrustedFolders();
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
if (folders.errors.length > 0) {
|
||||
for (const error of folders.errors) {
|
||||
console.error(
|
||||
`Error loading trusted folders config from ${error.path}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const trustedPaths: string[] = [];
|
||||
const untrustedPaths: string[] = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
switch (rule.trustLevel) {
|
||||
case TrustLevel.TRUST_FOLDER:
|
||||
trustedPaths.push(rule.path);
|
||||
break;
|
||||
case TrustLevel.TRUST_PARENT:
|
||||
trustedPaths.push(path.dirname(rule.path));
|
||||
break;
|
||||
case TrustLevel.DO_NOT_TRUST:
|
||||
untrustedPaths.push(rule.path);
|
||||
break;
|
||||
default:
|
||||
// Do nothing for unknown trust levels.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
for (const trustedPath of trustedPaths) {
|
||||
if (isWithinRoot(cwd, trustedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const untrustedPath of untrustedPaths) {
|
||||
if (path.normalize(cwd) === path.normalize(untrustedPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return folders.isPathTrusted(process.cwd());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user