mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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
|
// 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 {
|
import {
|
||||||
describe,
|
describe,
|
||||||
it,
|
it,
|
||||||
@@ -58,7 +58,9 @@ import {
|
|||||||
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
|
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
|
||||||
migrateSettingsToV1,
|
migrateSettingsToV1,
|
||||||
type Settings,
|
type Settings,
|
||||||
|
loadEnvironment,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
|
import { GEMINI_DIR } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
const MOCK_WORKSPACE_DIR = '/mock/workspace';
|
const MOCK_WORKSPACE_DIR = '/mock/workspace';
|
||||||
// Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency
|
// 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 {
|
export function loadEnvironment(settings: Settings): void {
|
||||||
const envFilePath = findEnvFile(process.cwd());
|
const envFilePath = findEnvFile(process.cwd());
|
||||||
|
|
||||||
|
if (!isWorkspaceTrusted(settings)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Cloud Shell environment variable handling
|
// Cloud Shell environment variable handling
|
||||||
if (process.env['CLOUD_SHELL'] === 'true') {
|
if (process.env['CLOUD_SHELL'] === 'true') {
|
||||||
setUpCloudShellEnvironment(envFilePath);
|
setUpCloudShellEnvironment(envFilePath);
|
||||||
|
|||||||
@@ -80,6 +80,52 @@ describe('Trusted Folders Loading', () => {
|
|||||||
expect(errors).toEqual([]);
|
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', () => {
|
it('should load user rules if only user file exists', () => {
|
||||||
const userPath = USER_TRUSTED_FOLDERS_PATH;
|
const userPath = USER_TRUSTED_FOLDERS_PATH;
|
||||||
(mockFsExistsSync as Mock).mockImplementation((p) => p === userPath);
|
(mockFsExistsSync as Mock).mockImplementation((p) => p === userPath);
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export interface TrustedFoldersFile {
|
|||||||
|
|
||||||
export class LoadedTrustedFolders {
|
export class LoadedTrustedFolders {
|
||||||
constructor(
|
constructor(
|
||||||
public user: TrustedFoldersFile,
|
readonly user: TrustedFoldersFile,
|
||||||
public errors: TrustedFoldersError[],
|
readonly errors: TrustedFoldersError[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get rules(): TrustRule[] {
|
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 {
|
setValue(path: string, trustLevel: TrustLevel): void {
|
||||||
this.user.config[path] = trustLevel;
|
this.user.config[path] = trustLevel;
|
||||||
saveTrustedFolders(this.user);
|
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 =
|
const folderTrustFeature =
|
||||||
settings.security?.folderTrust?.featureEnabled ?? false;
|
settings.security?.folderTrust?.featureEnabled ?? false;
|
||||||
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rules, errors } = loadTrustedFolders();
|
const folders = loadTrustedFolders();
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (folders.errors.length > 0) {
|
||||||
for (const error of errors) {
|
for (const error of folders.errors) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error loading trusted folders config from ${error.path}: ${error.message}`,
|
`Error loading trusted folders config from ${error.path}: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trustedPaths: string[] = [];
|
return folders.isPathTrusted(process.cwd());
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user