fix(trust): Refuse to load from untrusted process.cwd() sources; Add tests (#7323)

This commit is contained in:
Richie Foreman
2025-08-28 15:16:07 -04:00
committed by GitHub
parent cfc63d49ec
commit ecdea602a3
4 changed files with 158 additions and 44 deletions

View File

@@ -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');
});
});
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());
}