mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-10 05:10:59 -07:00
fix(core): remove broken PowerShell translation and fix native __write in Windows sandbox (#24571)
This commit is contained in:
committed by
GitHub
parent
fe4fcf89d9
commit
8c215c7a88
@@ -158,8 +158,8 @@ public class GeminiSandbox {
|
||||
|
||||
static int Main(string[] args) {
|
||||
if (args.Length < 3) {
|
||||
Console.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] <command> [args...]");
|
||||
Console.WriteLine("Internal commands: __read <path>, __write <path>");
|
||||
Console.Error.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] <command> [args...]");
|
||||
Console.Error.WriteLine("Internal commands: __read <path>, __write <path>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ public class GeminiSandbox {
|
||||
}
|
||||
|
||||
if (argIndex >= args.Length) {
|
||||
Console.WriteLine("Error: Missing command");
|
||||
Console.Error.WriteLine("Error: Missing command");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -196,13 +196,13 @@ public class GeminiSandbox {
|
||||
try {
|
||||
// 1. Duplicate Primary Token
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out hToken)) {
|
||||
Console.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
Console.Error.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a restricted token to strip administrative privileges
|
||||
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) {
|
||||
Console.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
Console.Error.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class GeminiSandbox {
|
||||
try {
|
||||
Marshal.StructureToPtr(tml, pTml, false);
|
||||
if (!SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize)) {
|
||||
Console.WriteLine("Error: SetTokenInformation failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
Console.Error.WriteLine("Error: SetTokenInformation failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
} finally {
|
||||
@@ -250,7 +250,7 @@ public class GeminiSandbox {
|
||||
// 4. Handle Internal Commands or External Process
|
||||
if (command == "__read") {
|
||||
if (argIndex + 1 >= args.Length) {
|
||||
Console.WriteLine("Error: Missing path for __read");
|
||||
Console.Error.WriteLine("Error: Missing path for __read");
|
||||
return 1;
|
||||
}
|
||||
string path = args[argIndex + 1];
|
||||
@@ -269,24 +269,31 @@ public class GeminiSandbox {
|
||||
});
|
||||
} else if (command == "__write") {
|
||||
if (argIndex + 1 >= args.Length) {
|
||||
Console.WriteLine("Error: Missing path for __write");
|
||||
Console.Error.WriteLine("Error: Missing path for __write");
|
||||
return 1;
|
||||
}
|
||||
string path = args[argIndex + 1];
|
||||
CheckForbidden(path, forbiddenPaths);
|
||||
return RunInImpersonation(hRestrictedToken, () => {
|
||||
try {
|
||||
using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), System.Text.Encoding.UTF8))
|
||||
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8)) {
|
||||
writer.Write(reader.ReadToEnd());
|
||||
|
||||
try {
|
||||
using (MemoryStream ms = new MemoryStream()) {
|
||||
// Buffer stdin before impersonation (as restricted token can't read the inherited pipe).
|
||||
using (Stream stdin = Console.OpenStandardInput()) {
|
||||
stdin.CopyTo(ms);
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
Console.Error.WriteLine("Error writing file: " + e.Message);
|
||||
return 1;
|
||||
|
||||
return RunInImpersonation(hRestrictedToken, () => {
|
||||
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) {
|
||||
ms.Position = 0;
|
||||
ms.CopyTo(fs);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Console.Error.WriteLine("Error during __write: " + e.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// External Process
|
||||
@@ -337,7 +344,7 @@ public class GeminiSandbox {
|
||||
|
||||
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
|
||||
if (!ImpersonateLoggedOnUser(hToken)) {
|
||||
Console.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
Console.Error.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -495,7 +495,7 @@ describe('WindowsSandboxManager', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should translate __write to PowerShell safely using environment variables', async () => {
|
||||
it('should pass __write directly to native helper', async () => {
|
||||
const filePath = path.join(testCwd, 'test.txt');
|
||||
fs.writeFileSync(filePath, '');
|
||||
const req: SandboxRequest = {
|
||||
@@ -508,16 +508,11 @@ describe('WindowsSandboxManager', () => {
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
// [network, cwd, --forbidden-manifest, manifestPath, command, ...args]
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
expect(result.args[7]).toBe('-Command');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
);
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(filePath);
|
||||
expect(result.args[4]).toBe('__write');
|
||||
expect(result.args[5]).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should safely handle special characters in __write path using environment variables', async () => {
|
||||
it('should safely handle special characters in __write path', async () => {
|
||||
const maliciousPath = path.join(testCwd, 'foo"; echo bar; ".txt');
|
||||
fs.writeFileSync(maliciousPath, '');
|
||||
const req: SandboxRequest = {
|
||||
@@ -529,16 +524,12 @@ describe('WindowsSandboxManager', () => {
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
);
|
||||
// The malicious path should be injected safely via environment variable, not interpolated in args
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(maliciousPath);
|
||||
// Native commands pass arguments directly; the binary handles quoting via QuoteArgument
|
||||
expect(result.args[4]).toBe('__write');
|
||||
expect(result.args[5]).toBe(maliciousPath);
|
||||
});
|
||||
|
||||
it('should translate __read to PowerShell safely using environment variables', async () => {
|
||||
it('should pass __read directly to native helper', async () => {
|
||||
const filePath = path.join(testCwd, 'test.txt');
|
||||
fs.writeFileSync(filePath, 'hello');
|
||||
const req: SandboxRequest = {
|
||||
@@ -550,12 +541,7 @@ describe('WindowsSandboxManager', () => {
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
expect(result.args[7]).toBe('-Command');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }',
|
||||
);
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(filePath);
|
||||
expect(result.args[4]).toBe('__read');
|
||||
expect(result.args[5]).toBe(filePath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,32 +217,10 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// Reject override attempts in plan mode
|
||||
verifySandboxOverrides(allowOverrides, req.policy);
|
||||
|
||||
let command = req.command;
|
||||
let args = req.args;
|
||||
let targetPathEnv: string | undefined;
|
||||
const command = req.command;
|
||||
const args = req.args;
|
||||
|
||||
// Translate virtual commands for sandboxed file system access
|
||||
if (command === '__read') {
|
||||
// Use PowerShell for safe argument passing via env var
|
||||
targetPathEnv = args[0] || '';
|
||||
command = 'PowerShell.exe';
|
||||
args = [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'& { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }',
|
||||
];
|
||||
} else if (command === '__write') {
|
||||
// Use PowerShell for piping stdin to a file via env var
|
||||
targetPathEnv = args[0] || '';
|
||||
command = 'PowerShell.exe';
|
||||
args = [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
];
|
||||
}
|
||||
// Native commands __read and __write are passed directly to GeminiSandbox.exe
|
||||
|
||||
const isYolo = this.options.modeConfig?.yolo ?? false;
|
||||
|
||||
@@ -427,9 +405,6 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
];
|
||||
|
||||
const finalEnv = { ...sanitizedEnv };
|
||||
if (targetPathEnv !== undefined) {
|
||||
finalEnv['GEMINI_TARGET_PATH'] = targetPathEnv;
|
||||
}
|
||||
|
||||
return {
|
||||
program,
|
||||
|
||||
Reference in New Issue
Block a user