fix(core): secure shell execution with AST validation

Replaces simplistic prefix-matching for shell command policies with robust Abstract Syntax Tree (AST) parsing using `bash-parser`.

Previously, policies for shell tools only checked if the command string started with an allowed prefix (e.g., `echo`), allowing trivial bypasses via shell operators like `&&` or `;` (e.g., `echo "ok" && rm -rf /`).

This update secures the execution pipeline by parsing the shell string and validating *every* extracted sub-command against the allowed policies.

Key changes:
- Integrated `bash-parser` to synchronously extract executable commands from pipelines, lists, and subshells.
- Updated `doesToolInvocationMatch` to enforce policy on all extracted sub-commands instead of just the string prefix.
- Enforced `coreTools` validation at execution time within `ShellTool` to prevent bypasses when tools are configured via `settings.json`.
- Updated the CLI `useShellCommandProcessor` to run human-input commands through the AST `PolicyEngine` check before spawning the process.
- Fixed asynchronous test flakiness in the CLI package caused by the new policy enforcement.
This commit is contained in:
galz10
2026-03-16 15:07:51 -07:00
parent dfe22aae21
commit 459db523e2
9 changed files with 557 additions and 61 deletions
@@ -14,6 +14,7 @@ import {
isBinary,
ShellExecutionService,
CoreToolCallStatus,
PolicyDecision,
} from '@google/gemini-cli-core';
import { type PartListUnion } from '@google/genai';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
@@ -298,6 +299,45 @@ export const useShellCommandProcessor = (
}
const executeCommand = async () => {
try {
const policyEngine = config.getPolicyEngine();
const { decision } = await policyEngine.check(
{ name: 'run_shell_command', args: { command: rawQuery } },
undefined,
);
if (decision === PolicyDecision.DENY) {
addItemToHistory(
{
type: 'error',
text: `Command cannot be run. Blocked command: "${rawQuery}". Reason: Blocked by policy.`,
},
userMessageTimestamp,
);
if (pwdFilePath && fs.existsSync(pwdFilePath)) {
fs.unlinkSync(pwdFilePath);
}
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
setShellInputFocused(false);
return;
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
addItemToHistory(
{
type: 'error',
text: `Policy validation error: ${errorMessage}`,
},
userMessageTimestamp,
);
if (pwdFilePath && fs.existsSync(pwdFilePath)) {
fs.unlinkSync(pwdFilePath);
}
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
setShellInputFocused(false);
return;
}
let cumulativeStdout: string | AnsiOutput = '';
let isBinaryStream = false;
let binaryBytesReceived = 0;