mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
fix(core): ensure sandbox approvals are correctly persisted and matched for proactive expansions (#24577)
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { SandboxPolicyManager } from './sandboxPolicyManager.js';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('SandboxPolicyManager', () => {
|
||||
const tempDir = path.join(os.tmpdir(), 'gemini-test-sandbox-policy');
|
||||
const configPath = path.join(tempDir, 'sandbox.toml');
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should add and retrieve session approvals', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
manager.addSessionApproval('ls', {
|
||||
fileSystem: { read: ['/tmp'], write: [] },
|
||||
network: false,
|
||||
});
|
||||
|
||||
const perms = manager.getCommandPermissions('ls');
|
||||
expect(perms.fileSystem?.read).toContain('/tmp');
|
||||
});
|
||||
|
||||
it('should protect against prototype pollution (session)', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
manager.addSessionApproval('__proto__', {
|
||||
fileSystem: { read: ['/POLLUTED'], write: [] },
|
||||
network: true,
|
||||
});
|
||||
|
||||
const perms = manager.getCommandPermissions('any-command');
|
||||
expect(perms.fileSystem?.read).not.toContain('/POLLUTED');
|
||||
});
|
||||
|
||||
it('should protect against prototype pollution (persistent)', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
manager.addPersistentApproval('constructor', {
|
||||
fileSystem: { read: ['/POLLUTED_PERSISTENT'], write: [] },
|
||||
network: true,
|
||||
});
|
||||
|
||||
const perms = manager.getCommandPermissions('constructor');
|
||||
expect(perms.fileSystem?.read).not.toContain('/POLLUTED_PERSISTENT');
|
||||
});
|
||||
|
||||
it('should lowercase command names for normalization', () => {
|
||||
const manager = new SandboxPolicyManager(configPath);
|
||||
manager.addSessionApproval('NPM', {
|
||||
fileSystem: { read: ['/node_modules'], write: [] },
|
||||
network: true,
|
||||
});
|
||||
|
||||
const perms = manager.getCommandPermissions('npm');
|
||||
expect(perms.fileSystem?.read).toContain('/node_modules');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { type SandboxPermissions } from '../services/sandboxManager.js';
|
||||
import { sanitizePaths } from '../services/sandboxManager.js';
|
||||
import { normalizeCommand } from '../utils/shell-utils.js';
|
||||
|
||||
export const SandboxModeConfigSchema = z.object({
|
||||
network: z.boolean(),
|
||||
@@ -104,6 +105,10 @@ export class SandboxPolicyManager {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
private isProtectedKey(key: string): boolean {
|
||||
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
||||
}
|
||||
|
||||
private loadConfig(): SandboxTomlSchemaType {
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
return SandboxPolicyManager.DEFAULT_CONFIG;
|
||||
@@ -154,8 +159,15 @@ export class SandboxPolicyManager {
|
||||
}
|
||||
|
||||
getCommandPermissions(commandName: string): SandboxPermissions {
|
||||
const persistent = this.config.commands[commandName];
|
||||
const session = this.sessionApprovals[commandName];
|
||||
const normalized = normalizeCommand(commandName);
|
||||
if (this.isProtectedKey(normalized)) {
|
||||
return {
|
||||
fileSystem: { read: [], write: [] },
|
||||
network: false,
|
||||
};
|
||||
}
|
||||
const persistent = this.config.commands[normalized];
|
||||
const session = this.sessionApprovals[normalized];
|
||||
|
||||
return {
|
||||
fileSystem: {
|
||||
@@ -176,25 +188,25 @@ export class SandboxPolicyManager {
|
||||
commandName: string,
|
||||
permissions: SandboxPermissions,
|
||||
): void {
|
||||
const existing = this.sessionApprovals[commandName] || {
|
||||
const normalized = normalizeCommand(commandName);
|
||||
if (this.isProtectedKey(normalized)) {
|
||||
return;
|
||||
}
|
||||
const existing = this.sessionApprovals[normalized] || {
|
||||
fileSystem: { read: [], write: [] },
|
||||
network: false,
|
||||
};
|
||||
|
||||
this.sessionApprovals[commandName] = {
|
||||
this.sessionApprovals[normalized] = {
|
||||
fileSystem: {
|
||||
read: Array.from(
|
||||
new Set([
|
||||
...(existing.fileSystem?.read ?? []),
|
||||
...(permissions.fileSystem?.read ?? []),
|
||||
]),
|
||||
),
|
||||
write: Array.from(
|
||||
new Set([
|
||||
...(existing.fileSystem?.write ?? []),
|
||||
...(permissions.fileSystem?.write ?? []),
|
||||
]),
|
||||
),
|
||||
read: sanitizePaths([
|
||||
...(existing.fileSystem?.read ?? []),
|
||||
...(permissions.fileSystem?.read ?? []),
|
||||
]),
|
||||
write: sanitizePaths([
|
||||
...(existing.fileSystem?.write ?? []),
|
||||
...(permissions.fileSystem?.write ?? []),
|
||||
]),
|
||||
},
|
||||
network: existing.network || permissions.network || false,
|
||||
};
|
||||
@@ -204,7 +216,11 @@ export class SandboxPolicyManager {
|
||||
commandName: string,
|
||||
permissions: SandboxPermissions,
|
||||
): void {
|
||||
const existing = this.config.commands[commandName] || {
|
||||
const normalized = normalizeCommand(commandName);
|
||||
if (this.isProtectedKey(normalized)) {
|
||||
return;
|
||||
}
|
||||
const existing = this.config.commands[normalized] || {
|
||||
allowed_paths: [],
|
||||
allow_network: false,
|
||||
};
|
||||
@@ -216,7 +232,7 @@ export class SandboxPolicyManager {
|
||||
];
|
||||
const newPaths = new Set(sanitizePaths(newPathsArray));
|
||||
|
||||
this.config.commands[commandName] = {
|
||||
this.config.commands[normalized] = {
|
||||
allowed_paths: Array.from(newPaths),
|
||||
allow_network: existing.allow_network || permissions.network || false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user