diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c32d1c033c..55e57004855e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -954,6 +954,7 @@ Docs: https://docs.openclaw.ai - Security/path resolution: prefer non-user-writable absolute helper binaries for OpenClaw CLI, ffmpeg, and OpenSSL resolution so PATH hijacks cannot replace trusted helpers with attacker-controlled executables. - Security/gateway command scopes: require `operator.admin` before Telegram target writeback and Talk Voice `/voice set` config writes persist through gateway message flows. - Security/OpenShell mirror: exclude workspace `hooks/` from mirror sync so untrusted sandbox files cannot become trusted host hooks on gateway startup. +- Exec env policy: block Mercurial config redirects, Rust compiler wrappers, and GNU make flag env vars in host exec sanitization so inherited env and request-scoped overrides cannot redirect build-tool execution. ## 2026.3.24-beta.2 diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index 49c5af30bab6..23833ce13873 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -28,6 +28,8 @@ enum HostEnvSecurityPolicy { "CC", "CXX", "CARGO_BUILD_RUSTC", + "CARGO_BUILD_RUSTC_WRAPPER", + "RUSTC_WRAPPER", "CMAKE_C_COMPILER", "CMAKE_CXX_COMPILER", "SHELL", @@ -44,9 +46,12 @@ enum HostEnvSecurityPolicy { "DOTNET_ADDITIONAL_DEPS", "GLIBC_TUNABLES", "MAVEN_OPTS", + "MAKEFLAGS", + "MFLAGS", "SBT_OPTS", "GRADLE_OPTS", - "ANT_OPTS" + "ANT_OPTS", + "HGRCPATH" ] static let blockedOverrideKeys: Set = [ @@ -83,6 +88,8 @@ enum HostEnvSecurityPolicy { "CGO_CFLAGS", "CGO_LDFLAGS", "GOFLAGS", + "MAKEFLAGS", + "MFLAGS", "CORECLR_PROFILER_PATH", "PHPRC", "PHP_INI_SCAN_DIR", @@ -134,7 +141,9 @@ enum HostEnvSecurityPolicy { "GOPRIVATE", "GOENV", "GOPATH", + "HGRCPATH", "PYTHONUSERBASE", + "RUSTC_WRAPPER", "VIRTUAL_ENV", "LUA_PATH", "LUA_CPATH", @@ -142,6 +151,7 @@ enum HostEnvSecurityPolicy { "GEM_PATH", "BUNDLE_GEMFILE", "COMPOSER_HOME", + "CARGO_BUILD_RUSTC_WRAPPER", "XDG_CONFIG_HOME", "AWS_CONFIG_FILE" ] diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index 700b218ab4ae..90f6132e47b6 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -22,6 +22,8 @@ "CC", "CXX", "CARGO_BUILD_RUSTC", + "CARGO_BUILD_RUSTC_WRAPPER", + "RUSTC_WRAPPER", "CMAKE_C_COMPILER", "CMAKE_CXX_COMPILER", "SHELL", @@ -38,9 +40,12 @@ "DOTNET_ADDITIONAL_DEPS", "GLIBC_TUNABLES", "MAVEN_OPTS", + "MAKEFLAGS", + "MFLAGS", "SBT_OPTS", "GRADLE_OPTS", - "ANT_OPTS" + "ANT_OPTS", + "HGRCPATH" ], "blockedOverrideKeys": [ "HOME", @@ -76,6 +81,8 @@ "CGO_CFLAGS", "CGO_LDFLAGS", "GOFLAGS", + "MAKEFLAGS", + "MFLAGS", "CORECLR_PROFILER_PATH", "PHPRC", "PHP_INI_SCAN_DIR", @@ -127,7 +134,9 @@ "GOPRIVATE", "GOENV", "GOPATH", + "HGRCPATH", "PYTHONUSERBASE", + "RUSTC_WRAPPER", "VIRTUAL_ENV", "LUA_PATH", "LUA_CPATH", @@ -135,6 +144,7 @@ "GEM_PATH", "BUNDLE_GEMFILE", "COMPOSER_HOME", + "CARGO_BUILD_RUSTC_WRAPPER", "XDG_CONFIG_HOME", "AWS_CONFIG_FILE" ], diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 98e86a1fc843..66e6b331b8b2 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -143,10 +143,14 @@ describe("isDangerousHostEnvVarName", () => { expect(isDangerousHostEnvVarName("cxx")).toBe(true); expect(isDangerousHostEnvVarName("CARGO_BUILD_RUSTC")).toBe(true); expect(isDangerousHostEnvVarName("cargo_build_rustc")).toBe(true); + expect(isDangerousHostEnvVarName("CARGO_BUILD_RUSTC_WRAPPER")).toBe(true); + expect(isDangerousHostEnvVarName("cargo_build_rustc_wrapper")).toBe(true); expect(isDangerousHostEnvVarName("CMAKE_C_COMPILER")).toBe(true); expect(isDangerousHostEnvVarName("cmake_c_compiler")).toBe(true); expect(isDangerousHostEnvVarName("CMAKE_CXX_COMPILER")).toBe(true); expect(isDangerousHostEnvVarName("cmake_cxx_compiler")).toBe(true); + expect(isDangerousHostEnvVarName("RUSTC_WRAPPER")).toBe(true); + expect(isDangerousHostEnvVarName("rustc_wrapper")).toBe(true); expect(isDangerousHostEnvVarName("SHELLOPTS")).toBe(true); expect(isDangerousHostEnvVarName("ps4")).toBe(true); expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true); @@ -168,12 +172,18 @@ describe("isDangerousHostEnvVarName", () => { expect(isDangerousHostEnvVarName("glibc_tunables")).toBe(true); expect(isDangerousHostEnvVarName("MAVEN_OPTS")).toBe(true); expect(isDangerousHostEnvVarName("maven_opts")).toBe(true); + expect(isDangerousHostEnvVarName("MAKEFLAGS")).toBe(true); + expect(isDangerousHostEnvVarName("makeflags")).toBe(true); + expect(isDangerousHostEnvVarName("MFLAGS")).toBe(true); + expect(isDangerousHostEnvVarName("mflags")).toBe(true); expect(isDangerousHostEnvVarName("SBT_OPTS")).toBe(true); expect(isDangerousHostEnvVarName("sbt_opts")).toBe(true); expect(isDangerousHostEnvVarName("GRADLE_OPTS")).toBe(true); expect(isDangerousHostEnvVarName("gradle_opts")).toBe(true); expect(isDangerousHostEnvVarName("ANT_OPTS")).toBe(true); expect(isDangerousHostEnvVarName("ant_opts")).toBe(true); + expect(isDangerousHostEnvVarName("HGRCPATH")).toBe(true); + expect(isDangerousHostEnvVarName("hgrcpath")).toBe(true); expect(isDangerousHostEnvVarName("HTTPS_PROXY")).toBe(false); expect(isDangerousHostEnvVarName("https_proxy")).toBe(false); expect(isDangerousHostEnvVarName("HTTP_PROXY")).toBe(false); @@ -210,6 +220,11 @@ describe("sanitizeHostExecEnv", () => { GIT_EXTERNAL_DIFF: "/tmp/pwn.sh", GIT_TEMPLATE_DIR: "/tmp/git-template", GIT_SEQUENCE_EDITOR: "/tmp/pwn-sequence-editor", + HGRCPATH: "/tmp/evil-hgrc", + CARGO_BUILD_RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", + RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", + MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", + MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", AWS_CONFIG_FILE: "/tmp/aws-config", LD_PRELOAD: "/tmp/pwn.so", OK: "1", @@ -242,8 +257,11 @@ describe("sanitizeHostExecEnv", () => { CC: "/tmp/evil-cc", CXX: "/tmp/evil-cxx", CARGO_BUILD_RUSTC: "/tmp/evil-rustc", + CARGO_BUILD_RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", CMAKE_C_COMPILER: "/tmp/evil-c-compiler", CMAKE_CXX_COMPILER: "/tmp/evil-cxx-compiler", + RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", + HGRCPATH: "/tmp/evil-hgrc", GIT_SSH_COMMAND: "touch /tmp/pwned", GIT_EDITOR: "/tmp/git-editor", GIT_EXEC_PATH: "/tmp/git-exec-path", @@ -295,6 +313,8 @@ describe("sanitizeHostExecEnv", () => { PS4: "$(touch /tmp/pwned)", CLASSPATH: "/tmp/evil-classpath", GOFLAGS: "-mod=mod", + MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", + MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", PHPRC: "/tmp/evil-php.ini", XDG_CONFIG_HOME: "/tmp/evil-config", SAFE: "ok", @@ -309,8 +329,11 @@ describe("sanitizeHostExecEnv", () => { expect(env.CC).toBeUndefined(); expect(env.CXX).toBeUndefined(); expect(env.CARGO_BUILD_RUSTC).toBeUndefined(); + expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBeUndefined(); expect(env.CMAKE_C_COMPILER).toBeUndefined(); expect(env.CMAKE_CXX_COMPILER).toBeUndefined(); + expect(env.RUSTC_WRAPPER).toBeUndefined(); + expect(env.HGRCPATH).toBeUndefined(); expect(env.GIT_TEMPLATE_DIR).toBeUndefined(); expect(env.GIT_SEQUENCE_EDITOR).toBeUndefined(); expect(env.AWS_CONFIG_FILE).toBeUndefined(); @@ -324,6 +347,8 @@ describe("sanitizeHostExecEnv", () => { expect(env.PS4).toBeUndefined(); expect(env.CLASSPATH).toBeUndefined(); expect(env.GOFLAGS).toBeUndefined(); + expect(env.MAKEFLAGS).toBeUndefined(); + expect(env.MFLAGS).toBeUndefined(); expect(env.PHPRC).toBeUndefined(); expect(env.XDG_CONFIG_HOME).toBeUndefined(); expect(env.YARN_RC_FILENAME).toBe(".trusted-yarnrc.yml"); @@ -524,8 +549,18 @@ describe("isDangerousHostEnvOverrideVarName", () => { expect(isDangerousHostEnvOverrideVarName("virtual_env")).toBe(true); expect(isDangerousHostEnvOverrideVarName("CLASSPATH")).toBe(true); expect(isDangerousHostEnvOverrideVarName("classpath")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("MAKEFLAGS")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("makeflags")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("MFLAGS")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("mflags")).toBe(true); expect(isDangerousHostEnvOverrideVarName("GOFLAGS")).toBe(true); expect(isDangerousHostEnvOverrideVarName("goflags")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("HGRCPATH")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("hgrcpath")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("RUSTC_WRAPPER")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("rustc_wrapper")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("CARGO_BUILD_RUSTC_WRAPPER")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("cargo_build_rustc_wrapper")).toBe(true); expect(isDangerousHostEnvOverrideVarName("CORECLR_PROFILER_PATH")).toBe(true); expect(isDangerousHostEnvOverrideVarName("coreclr_profiler_path")).toBe(true); expect(isDangerousHostEnvOverrideVarName("XDG_CONFIG_HOME")).toBe(true); @@ -547,6 +582,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { overrides: { PATH: "/tmp/evil", CXX: "/tmp/evil-cxx", + CARGO_BUILD_RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", CARGO_REGISTRIES_CRATES_IO_INDEX: "https://example.invalid/crates.io-index", CMAKE_C_COMPILER: "/tmp/evil-c-compiler", CLASSPATH: "/tmp/evil-classpath", @@ -582,7 +618,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { GOPRIVATE: "example.invalid/*", GOENV: "/tmp/evil-goenv", GOPATH: "/tmp/evil-go", + HGRCPATH: "/tmp/evil-hgrc", + MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", + MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", PYTHONUSERBASE: "/tmp/evil-python-userbase", + RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", VIRTUAL_ENV: "/tmp/evil-venv", YARN_RC_FILENAME: ".evil-yarnrc.yml", HTTPS_PROXY: "http://proxy.example.test:8080", @@ -597,6 +637,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.rejectedOverrideBlockedKeys).toEqual([ "C_INCLUDE_PATH", + "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_REGISTRIES_CRATES_IO_INDEX", "CLASSPATH", "CMAKE_C_COMPILER", @@ -618,8 +659,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { "GOPATH", "GOPRIVATE", "GOPROXY", + "HGRCPATH", "HTTPS_PROXY", "LIBRARY_PATH", + "MAKEFLAGS", + "MFLAGS", "NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED", "OBJC_INCLUDE_PATH", @@ -632,6 +676,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { "PIP_TRUSTED_HOST", "PYTHONUSERBASE", "REQUESTS_CA_BUNDLE", + "RUSTC_WRAPPER", "SSL_CERT_DIR", "SSL_CERT_FILE", "UV_DEFAULT_INDEX", @@ -648,6 +693,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.CLASSPATH).toBeUndefined(); expect(result.env.CXX).toBeUndefined(); expect(result.env.CMAKE_C_COMPILER).toBeUndefined(); + expect(result.env.CARGO_BUILD_RUSTC_WRAPPER).toBeUndefined(); expect(result.env.CARGO_REGISTRIES_CRATES_IO_INDEX).toBeUndefined(); expect(result.env.PIP_INDEX_URL).toBeUndefined(); expect(result.env.PIP_PYPI_URL).toBeUndefined(); @@ -684,9 +730,13 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.GOPRIVATE).toBeUndefined(); expect(result.env.GOENV).toBeUndefined(); expect(result.env.GOPATH).toBeUndefined(); + expect(result.env.HGRCPATH).toBeUndefined(); expect(result.env.HTTPS_PROXY).toBeUndefined(); + expect(result.env.MAKEFLAGS).toBeUndefined(); + expect(result.env.MFLAGS).toBeUndefined(); expect(result.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined(); expect(result.env.PYTHONUSERBASE).toBeUndefined(); + expect(result.env.RUSTC_WRAPPER).toBeUndefined(); expect(result.env.VIRTUAL_ENV).toBeUndefined(); expect(result.env.YARN_RC_FILENAME).toBeUndefined(); }); @@ -1100,3 +1150,52 @@ describe("compiler override exploit regression", () => { } }); }); + +describe("make env exploit regression", () => { + it("blocks MAKEFLAGS overrides so make cannot evaluate shell payloads from env", async () => { + const makePath = getSystemMakePath(); + if (!makePath) { + return; + } + + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), `openclaw-makeflags-override-${process.pid}-${Date.now()}-`), + ); + const exploitPath = path.join(tempDir, "evil-makeflags.sh"); + const marker = path.join(os.tmpdir(), `openclaw-makeflags-marker-${process.pid}-${Date.now()}`); + + try { + clearMarker(marker); + fs.writeFileSync(path.join(tempDir, "Makefile"), "all:\n\t@:\n", "utf8"); + fs.writeFileSync(exploitPath, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`, "utf8"); + fs.chmodSync(exploitPath, 0o755); + + const exploitValue = `--eval=$(shell ${exploitPath})`; + const baseEnv = { + PATH: process.env.PATH ?? "/usr/bin:/bin", + }; + + await runMakeCommand(makePath, tempDir, { + ...baseEnv, + MAKEFLAGS: exploitValue, + }); + + expect(fs.existsSync(marker)).toBe(true); + clearMarker(marker); + + const safeEnv = sanitizeHostExecEnv({ + baseEnv, + overrides: { + MAKEFLAGS: exploitValue, + }, + }); + + await runMakeCommand(makePath, tempDir, safeEnv); + + expect(fs.existsSync(marker)).toBe(false); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + fs.rmSync(marker, { force: true }); + } + }); +});