mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-04 08:54:28 -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
stripShellWrapper,
|
||||
parseCommandDetails,
|
||||
hasRedirection,
|
||||
detectCommandSubstitution,
|
||||
normalizeCommand,
|
||||
escapeShellArg,
|
||||
} from '../utils/shell-utils.js';
|
||||
@@ -443,6 +444,18 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
} = options;
|
||||
const strippedCommand = stripShellWrapper(this.params.command);
|
||||
|
||||
if (detectCommandSubstitution(strippedCommand)) {
|
||||
return {
|
||||
llmContent:
|
||||
'Command injection detected: command substitution syntax ' +
|
||||
'($(), backticks, <() or >()) found in command arguments. ' +
|
||||
'On PowerShell, @() array subexpressions and $() subexpressions are also blocked. ' +
|
||||
'This is a security risk and the command was blocked.',
|
||||
returnDisplay:
|
||||
'Blocked: command substitution detected in shell command.',
|
||||
};
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return {
|
||||
llmContent: 'Command was cancelled by user before it could start.',
|
||||
|
||||
Reference in New Issue
Block a user