fix(security): block safeBins shell expansion

This commit is contained in:
Peter Steinberger
2026-02-14 19:42:52 +01:00
parent a73ccf2b53
commit 77b89719d5
8 changed files with 266 additions and 5 deletions
+22
View File
@@ -15,6 +15,7 @@ import {
recordAllowlistUse,
resolveExecApprovals,
resolveExecApprovalsFromFile,
buildSafeShellCommand,
} from "../infra/exec-approvals.js";
import { buildNodeShellCommand } from "../infra/node-shell.js";
import {
@@ -170,6 +171,7 @@ export function createExecTool(
const maxOutput = DEFAULT_MAX_OUTPUT;
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
const warnings: string[] = [];
let execCommandOverride: string | undefined;
const backgroundRequested = params.background === true;
const yieldRequested = typeof params.yieldMs === "number";
if (!allowBackground && (backgroundRequested || yieldRequested)) {
@@ -804,6 +806,25 @@ export function createExecTool(
throw new Error("exec denied: allowlist miss");
}
// If allowlist is satisfied only via safeBins (no explicit allowlist match),
// run a sanitized `shell -c` command that disables glob/var expansion by
// forcing every argv token to be literal via single-quoting.
if (
hostSecurity === "allowlist" &&
analysisOk &&
allowlistSatisfied &&
allowlistMatches.length === 0
) {
const safe = buildSafeShellCommand({
command: params.command,
platform: process.platform,
});
if (!safe.ok || !safe.command) {
throw new Error(`exec denied: safeBins sanitize failed (${safe.reason ?? "unknown"})`);
}
execCommandOverride = safe.command;
}
if (allowlistMatches.length > 0) {
const seen = new Set<string>();
for (const match of allowlistMatches) {
@@ -828,6 +849,7 @@ export function createExecTool(
const usePty = params.pty === true && !sandbox;
const run = await runExecProcess({
command: params.command,
execCommand: execCommandOverride,
workdir,
env,
sandbox,