Skip to content

Commit 1dcef7b

Browse files
authored
Infra: block GIT_EXEC_PATH in host env sanitizer (openclaw#43685)
* Infra: block GIT_EXEC_PATH in host env sanitizer * Changelog: note host env hardening
1 parent 18f1585 commit 1dcef7b

File tree

4 files changed

+60
-0
lines changed

4 files changed

+60
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ Docs: https://docs.openclaw.ai
129129
- Agents/fallback cooldown probing: cap cooldown-bypass probing to one attempt per provider per fallback run so multi-model same-provider cooldown chains can continue to cross-provider fallbacks instead of repeatedly stalling on duplicate cooldown probes. (#41711) Thanks @cgdusek.
130130
- Telegram/direct delivery: bridge direct delivery sends to internal `message:sent` hooks so internal hook listeners observe successful Telegram deliveries. (#40185) Thanks @vincentkoc.
131131
- Dependencies: refresh workspace dependencies except the pinned Carbon package, and harden ACP session-config writes against non-string SDK values so newer ACP clients fail fast instead of tripping type/runtime mismatches.
132+
- Security/host env: block inherited `GIT_EXEC_PATH` from sanitized host exec environments so Git helper resolution cannot be steered by host environment state. (#43685) Thanks @vincentkoc.
132133
- Telegram/polling restarts: clear bounded cleanup timeout handles after `runner.stop()` and `bot.stop()` settle so stall recovery no longer leaves stray 15-second timers behind on clean shutdown. (#43188) thanks @kyohwang.
133134
- Browser/proxy attachments: restore the shared media-store size cap for persisted browser proxy files so oversized payloads are rejected instead of overriding the intended 5 MB limit. (#43684) Thanks @vincentkoc.
134135

apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum HostEnvSecurityPolicy {
1717
"BASH_ENV",
1818
"ENV",
1919
"GIT_EXTERNAL_DIFF",
20+
"GIT_EXEC_PATH",
2021
"SHELL",
2122
"SHELLOPTS",
2223
"PS4",

src/infra/host-env-security-policy.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"BASH_ENV",
1212
"ENV",
1313
"GIT_EXTERNAL_DIFF",
14+
"GIT_EXEC_PATH",
1415
"SHELL",
1516
"SHELLOPTS",
1617
"PS4",

src/infra/host-env-security.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe("isDangerousHostEnvVarName", () => {
1818
expect(isDangerousHostEnvVarName("bash_env")).toBe(true);
1919
expect(isDangerousHostEnvVarName("SHELL")).toBe(true);
2020
expect(isDangerousHostEnvVarName("GIT_EXTERNAL_DIFF")).toBe(true);
21+
expect(isDangerousHostEnvVarName("git_exec_path")).toBe(true);
2122
expect(isDangerousHostEnvVarName("SHELLOPTS")).toBe(true);
2223
expect(isDangerousHostEnvVarName("ps4")).toBe(true);
2324
expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true);
@@ -60,6 +61,7 @@ describe("sanitizeHostExecEnv", () => {
6061
ZDOTDIR: "/tmp/evil-zdotdir",
6162
BASH_ENV: "/tmp/pwn.sh",
6263
GIT_SSH_COMMAND: "touch /tmp/pwned",
64+
GIT_EXEC_PATH: "/tmp/git-exec-path",
6365
EDITOR: "/tmp/editor",
6466
NPM_CONFIG_USERCONFIG: "/tmp/npmrc",
6567
GIT_CONFIG_GLOBAL: "/tmp/gitconfig",
@@ -73,6 +75,7 @@ describe("sanitizeHostExecEnv", () => {
7375
expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE);
7476
expect(env.BASH_ENV).toBeUndefined();
7577
expect(env.GIT_SSH_COMMAND).toBeUndefined();
78+
expect(env.GIT_EXEC_PATH).toBeUndefined();
7679
expect(env.EDITOR).toBeUndefined();
7780
expect(env.NPM_CONFIG_USERCONFIG).toBeUndefined();
7881
expect(env.GIT_CONFIG_GLOBAL).toBeUndefined();
@@ -211,6 +214,60 @@ describe("shell wrapper exploit regression", () => {
211214
});
212215

213216
describe("git env exploit regression", () => {
217+
it("blocks inherited GIT_EXEC_PATH so git cannot execute helper payloads", async () => {
218+
if (process.platform === "win32") {
219+
return;
220+
}
221+
const gitPath = "/usr/bin/git";
222+
if (!fs.existsSync(gitPath)) {
223+
return;
224+
}
225+
226+
const helperDir = fs.mkdtempSync(
227+
path.join(os.tmpdir(), `openclaw-git-exec-path-${process.pid}-${Date.now()}-`),
228+
);
229+
const helperPath = path.join(helperDir, "git-remote-https");
230+
const marker = path.join(
231+
os.tmpdir(),
232+
`openclaw-git-exec-path-marker-${process.pid}-${Date.now()}`,
233+
);
234+
try {
235+
fs.unlinkSync(marker);
236+
} catch {
237+
// no-op
238+
}
239+
fs.writeFileSync(helperPath, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\nexit 1\n`, "utf8");
240+
fs.chmodSync(helperPath, 0o755);
241+
242+
const target = "https://127.0.0.1:1/does-not-matter";
243+
const unsafeEnv = {
244+
PATH: process.env.PATH ?? "/usr/bin:/bin",
245+
GIT_EXEC_PATH: helperDir,
246+
GIT_TERMINAL_PROMPT: "0",
247+
};
248+
249+
await new Promise<void>((resolve) => {
250+
const child = spawn(gitPath, ["ls-remote", target], { env: unsafeEnv, stdio: "ignore" });
251+
child.once("error", () => resolve());
252+
child.once("close", () => resolve());
253+
});
254+
255+
expect(fs.existsSync(marker)).toBe(true);
256+
fs.unlinkSync(marker);
257+
258+
const safeEnv = sanitizeHostExecEnv({
259+
baseEnv: unsafeEnv,
260+
});
261+
262+
await new Promise<void>((resolve) => {
263+
const child = spawn(gitPath, ["ls-remote", target], { env: safeEnv, stdio: "ignore" });
264+
child.once("error", () => resolve());
265+
child.once("close", () => resolve());
266+
});
267+
268+
expect(fs.existsSync(marker)).toBe(false);
269+
});
270+
214271
it("blocks GIT_SSH_COMMAND override so git cannot execute helper payloads", async () => {
215272
if (process.platform === "win32") {
216273
return;

0 commit comments

Comments
 (0)