mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-16 13:00:39 -07:00
Fix/command injection shell (#24170)
Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
committed by
GitHub
parent
1c43deee07
commit
2a52611e71
@@ -210,7 +210,7 @@ describe('ShellTool', () => {
|
||||
mockShellOutputCallback = callback;
|
||||
const match = cmd.match(/pgrep -g 0 >([^ ]+)/);
|
||||
if (match) {
|
||||
extractedTmpFile = match[1].replace(/['"]/g, ''); // remove any quotes if present
|
||||
extractedTmpFile = match[1].replace(/['"]/g, '');
|
||||
}
|
||||
return {
|
||||
pid: 12345,
|
||||
@@ -994,7 +994,6 @@ EOF`;
|
||||
const result = await promise;
|
||||
expect(result.llmContent).not.toContain('Process Group PGID:');
|
||||
});
|
||||
|
||||
it('should have minimal output for successful command', async () => {
|
||||
const invocation = shellTool.build({ command: 'echo hello' });
|
||||
const promise = invocation.execute({ abortSignal: mockAbortSignal });
|
||||
@@ -1222,4 +1221,399 @@ EOF`;
|
||||
expect(schema.description).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('command injection detection', () => {
|
||||
it('should block $() command substitution', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo $(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block backtick command substitution', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo `whoami`' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow normal commands without substitution', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: 'hello',
|
||||
rawOutput: Buffer.from('hello'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo hello' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow single quoted strings with special chars', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(not substituted)',
|
||||
rawOutput: Buffer.from('$(not substituted)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({
|
||||
command: "echo '$(not substituted)'",
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow escaped backtick outside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: 'hello',
|
||||
rawOutput: Buffer.from('hello'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo \\`hello\\`' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block $() inside double quotes', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo "$(whoami)"' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block >() process substitution', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo >(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow $() inside single quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({
|
||||
command: "echo '$(whoami)'",
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
it('should block PowerShell @() array subexpression', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo @(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block PowerShell $() subexpression', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo $(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow PowerShell single quoted strings', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({
|
||||
command: "echo '$(whoami)'",
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
it('should allow escaped substitution outside quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo \\$(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow process substitution inside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '<(whoami)',
|
||||
rawOutput: Buffer.from('<(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo "<(whoami)"' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block process substitution without quotes', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo <(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow escaped $() outside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo \\$(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow output process substitution inside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '<(whoami)',
|
||||
rawOutput: Buffer.from('<(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo "<(whoami)"' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should block <() process substitution without quotes', async () => {
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo <(whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
it('should block PowerShell bare () grouping operator', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo (whoami)' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow escaped $() inside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo "\\$(whoami)"' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow escaped substitution inside double quotes', async () => {
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: '$(whoami)',
|
||||
rawOutput: Buffer.from('$(whoami)'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({ command: 'echo "\\$(whoami)"' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow PowerShell keyword with flag e.g. switch -regex ($x)', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: 'result',
|
||||
rawOutput: Buffer.from('result'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({
|
||||
command: 'switch -regex ($x) { "a" { 1 } }',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should allow PowerShell nested parentheses e.g. if ((condition))', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
|
||||
pid: 12345,
|
||||
result: Promise.resolve({
|
||||
output: 'result',
|
||||
rawOutput: Buffer.from('result'),
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
executionMethod: 'child_process',
|
||||
backgrounded: false,
|
||||
}),
|
||||
}));
|
||||
const tool = new ShellTool(mockConfig, createMockMessageBus());
|
||||
const invocation = tool.build({
|
||||
command: 'if ((condition)) { Write-Host ok }',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
expect(result.returnDisplay).not.toContain('Blocked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user