harden sandbox escape vectors for bash disabled/restricted modes (#257)

* harden sandbox escape vectors for bash disabled/restricted modes

block git config injection (-c flag as subcommand), dangerous subcommands
(config, submodule, rebase, bisect), code-executing arg flags (--exec,
--extcmd), .gitattributes/.gitmodules writes, and package lifecycle scripts.
add retry logic to test runner for transient failures. add security unit
tests and adhoc attack tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* only filter subcommands in nobash, remove nobash from ui

* use regex matching

* iterate on tests

* simplify githooks

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
David Blass
2026-02-11 02:02:41 +00:00
committed by pullfrog[bot]
parent f37d02b292
commit bc28c658f2
32 changed files with 2515 additions and 1048 deletions
+5 -4
View File
@@ -40,8 +40,9 @@ type SafeGitSubcommand = "fetch" | "push";
type GitAuthOptions = {
token: string;
cwd?: string;
// restricted bash mode: agents can write to .git/hooks/, so we disable hooks
// to prevent token exfiltration via malicious hooks reading GIT_CONFIG_PARAMETERS
// when true, disables hooks during authenticated git operations to prevent
// token exfiltration via malicious hooks reading GIT_CONFIG_PARAMETERS.
// should be true whenever bash is not "enabled" (both restricted and disabled).
restricted?: boolean;
};
@@ -124,8 +125,8 @@ export function $git(
const gitPath = verifyGitBinary();
const cwd = options.cwd ?? process.cwd();
// SECURITY: disable hooks in restricted mode to prevent token exfiltration
// agents could write malicious .git/hooks/pre-push that reads GIT_CONFIG_PARAMETERS
// SECURITY: disable hooks during authenticated operations to prevent token exfiltration.
// in restricted mode, agents can write .git/hooks/ via bash; in disabled mode, defense-in-depth.
if (options.restricted) {
const hasHooksOverride = args.some(
(arg) => arg.toLowerCase().includes("hookspath") || arg.toLowerCase().includes("hooks")
+17 -10
View File
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { PayloadEvent } from "../external.ts";
import type { BashPermission, PayloadEvent } from "../external.ts";
import { checkoutPrBranch } from "../mcp/checkout.ts";
import type { ToolState } from "../mcp/server.ts";
import { log } from "./cli.ts";
@@ -51,8 +51,11 @@ export interface GitContext {
name: string;
octokit: OctokitWithPlugins;
toolState: ToolState;
// restricted bash mode: disables git hooks to prevent token exfiltration
restricted: boolean;
// bash permission level — controls hook and security behavior:
// enabled: full bash, hooks run, no restrictions
// restricted: MCP bash in stripped env, hooks run, token protection on auth ops
// disabled: no bash, hooks disabled globally, all code execution paths blocked
bash: BashPermission;
postCheckoutScript: string | null;
}
@@ -104,13 +107,17 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
log.debug(`» git user already configured (${currentEmail}), skipping`);
}
// disable git hooks for predictability - prevents pre-commit hooks
// from blocking commits or causing unexpected side effects
execSync("git config --local core.hooksPath /dev/null", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("» git hooks disabled");
// SECURITY: disable git hooks when bash is disabled to prevent code execution.
// in restricted mode, hooks run in the stripped sandbox — that's fine.
// in enabled mode, the agent has full bash anyway.
// in disabled mode, hooks are the primary code-execution escape vector.
if (params.bash === "disabled") {
execSync("git config --local core.hooksPath /dev/null", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("» git hooks disabled (bash=disabled)");
}
} catch (error) {
// If git config fails, log warning but don't fail the action
// This can happen if we're not in a git repo or git isn't available