Infra: block additional host exec env keys (#55977)

This commit is contained in:
Jacob Tomlinson
2026-03-27 11:50:37 -07:00
committed by GitHub
parent fdbcfced84
commit 6eb82fba3c
3 changed files with 127 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ enum HostEnvSecurityPolicy {
"ENV",
"GIT_EXTERNAL_DIFF",
"GIT_EXEC_PATH",
"GIT_TEMPLATE_DIR",
"SHELL",
"SHELLOPTS",
"PS4",
@@ -34,7 +35,8 @@ enum HostEnvSecurityPolicy {
"MAVEN_OPTS",
"SBT_OPTS",
"GRADLE_OPTS",
"ANT_OPTS"
"ANT_OPTS",
"AWS_CONFIG_FILE"
]
static let blockedOverrideKeys: Set<String> = [

View File

@@ -12,6 +12,7 @@
"ENV",
"GIT_EXTERNAL_DIFF",
"GIT_EXEC_PATH",
"GIT_TEMPLATE_DIR",
"SHELL",
"SHELLOPTS",
"PS4",
@@ -28,7 +29,8 @@
"MAVEN_OPTS",
"SBT_OPTS",
"GRADLE_OPTS",
"ANT_OPTS"
"ANT_OPTS",
"AWS_CONFIG_FILE"
],
"blockedOverrideKeys": [
"HOME",

View File

@@ -37,6 +37,34 @@ async function runGitLsRemote(gitPath: string, target: string, env: NodeJS.Proce
});
}
async function runGitCommand(
gitPath: string,
args: string[],
options?: {
cwd?: string;
env?: NodeJS.ProcessEnv;
},
) {
await new Promise<void>((resolve) => {
const child = spawn(gitPath, args, {
cwd: options?.cwd,
env: options?.env,
stdio: "ignore",
});
child.once("error", () => resolve());
child.once("close", () => resolve());
});
}
async function runGitClone(
gitPath: string,
source: string,
destination: string,
env: NodeJS.ProcessEnv,
) {
await runGitCommand(gitPath, ["clone", source, destination], { env });
}
describe("isDangerousHostEnvVarName", () => {
it("matches dangerous keys and prefixes case-insensitively", () => {
expect(isDangerousHostEnvVarName("BASH_ENV")).toBe(true);
@@ -44,6 +72,8 @@ describe("isDangerousHostEnvVarName", () => {
expect(isDangerousHostEnvVarName("SHELL")).toBe(true);
expect(isDangerousHostEnvVarName("GIT_EXTERNAL_DIFF")).toBe(true);
expect(isDangerousHostEnvVarName("git_exec_path")).toBe(true);
expect(isDangerousHostEnvVarName("GIT_TEMPLATE_DIR")).toBe(true);
expect(isDangerousHostEnvVarName("git_template_dir")).toBe(true);
expect(isDangerousHostEnvVarName("SHELLOPTS")).toBe(true);
expect(isDangerousHostEnvVarName("ps4")).toBe(true);
expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true);
@@ -71,6 +101,8 @@ describe("isDangerousHostEnvVarName", () => {
expect(isDangerousHostEnvVarName("gradle_opts")).toBe(true);
expect(isDangerousHostEnvVarName("ANT_OPTS")).toBe(true);
expect(isDangerousHostEnvVarName("ant_opts")).toBe(true);
expect(isDangerousHostEnvVarName("AWS_CONFIG_FILE")).toBe(true);
expect(isDangerousHostEnvVarName("aws_config_file")).toBe(true);
expect(isDangerousHostEnvVarName("PATH")).toBe(false);
expect(isDangerousHostEnvVarName("FOO")).toBe(false);
expect(isDangerousHostEnvVarName("GRADLE_USER_HOME")).toBe(false);
@@ -84,6 +116,8 @@ describe("sanitizeHostExecEnv", () => {
PATH: "/usr/bin:/bin",
BASH_ENV: "/tmp/pwn.sh",
GIT_EXTERNAL_DIFF: "/tmp/pwn.sh",
GIT_TEMPLATE_DIR: "/tmp/git-template",
AWS_CONFIG_FILE: "/tmp/aws-config",
LD_PRELOAD: "/tmp/pwn.so",
OK: "1",
},
@@ -126,6 +160,8 @@ describe("sanitizeHostExecEnv", () => {
expect(env.PATH).toBe("/usr/bin:/bin");
expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE);
expect(env.BASH_ENV).toBeUndefined();
expect(env.GIT_TEMPLATE_DIR).toBeUndefined();
expect(env.AWS_CONFIG_FILE).toBeUndefined();
expect(env.GIT_SSH_COMMAND).toBeUndefined();
expect(env.GIT_EXEC_PATH).toBeUndefined();
expect(env.EDITOR).toBeUndefined();
@@ -426,6 +462,91 @@ describe("git env exploit regression", () => {
}
});
it("blocks inherited GIT_TEMPLATE_DIR so git clone cannot install hook payloads", async () => {
const gitPath = getSystemGitPath();
if (!gitPath) {
return;
}
const repoDir = fs.mkdtempSync(
path.join(os.tmpdir(), `openclaw-git-template-source-${process.pid}-${Date.now()}-`),
);
const cloneDir = path.join(
os.tmpdir(),
`openclaw-git-template-clone-${process.pid}-${Date.now()}`,
);
const safeCloneDir = path.join(
os.tmpdir(),
`openclaw-git-template-safe-clone-${process.pid}-${Date.now()}`,
);
const templateDir = fs.mkdtempSync(
path.join(os.tmpdir(), `openclaw-git-template-dir-${process.pid}-${Date.now()}-`),
);
const hooksDir = path.join(templateDir, "hooks");
const marker = path.join(
os.tmpdir(),
`openclaw-git-template-marker-${process.pid}-${Date.now()}`,
);
try {
fs.mkdirSync(hooksDir, { recursive: true });
clearMarker(marker);
fs.writeFileSync(
path.join(hooksDir, "post-checkout"),
`#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`,
"utf8",
);
fs.chmodSync(path.join(hooksDir, "post-checkout"), 0o755);
await runGitCommand(gitPath, ["init", repoDir]);
await runGitCommand(
gitPath,
[
"-C",
repoDir,
"-c",
"user.name=OpenClaw Test",
"-c",
"user.email=test@example.com",
"commit",
"--allow-empty",
"-m",
"init",
],
{
env: {
PATH: process.env.PATH ?? "/usr/bin:/bin",
},
},
);
const unsafeEnv = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
GIT_TEMPLATE_DIR: templateDir,
GIT_TERMINAL_PROMPT: "0",
};
await runGitClone(gitPath, repoDir, cloneDir, unsafeEnv);
expect(fs.existsSync(marker)).toBe(true);
clearMarker(marker);
const safeEnv = sanitizeHostExecEnv({
baseEnv: unsafeEnv,
});
await runGitClone(gitPath, repoDir, safeCloneDir, safeEnv);
expect(fs.existsSync(marker)).toBe(false);
} finally {
fs.rmSync(repoDir, { recursive: true, force: true });
fs.rmSync(cloneDir, { recursive: true, force: true });
fs.rmSync(safeCloneDir, { recursive: true, force: true });
fs.rmSync(templateDir, { recursive: true, force: true });
fs.rmSync(marker, { force: true });
}
});
it("blocks GIT_SSH_COMMAND override so git cannot execute helper payloads", async () => {
const gitPath = getSystemGitPath();
if (!gitPath) {