diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index e45261cda2e..a24d5c77726 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -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 = [ diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index 2f6cd25bde6..c97df3e48d4 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -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", diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index f326a0c75ed..98fbb7103b4 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -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((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) {