From eb8de6715f02949c21c4e895fffc8a6dcb00975c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 31 Mar 2026 19:37:43 +0900 Subject: [PATCH] fix(exec): block risky host env overrides (#58209) * fix(exec): block risky host env overrides * fix(exec): block GOPRIVATE host env overrides --- CHANGELOG.md | 1 + .../HostEnvSecurityPolicy.generated.swift | 26 ++++ src/infra/host-env-security-policy.json | 26 ++++ src/infra/host-env-security.test.ts | 147 ++++++++++++++++++ 4 files changed, 200 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef24a7c1ae..7d6ee328938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai - Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman. - Memory/QMD: preserve explicit `start_line` and `end_line` metadata from mcporter query results so `memory search` hits keep the real snippet offsets instead of falling back to the snippet header. (#47960) Thanks @vincentkoc. - LINE/ACP: add current-conversation binding and inbound binding-routing parity so `/acp spawn ... --thread here`, configured ACP bindings, and active conversation-bound ACP sessions work on LINE like the other conversation channels. +- Host exec/env: block additional request-scoped env overrides that can redirect Docker endpoints, trust roots, compiler include paths, package resolution, or Python environment roots during approved host runs. Thanks @tdjackey and @vincentkoc. - LINE/markdown: preserve underscores inside Latin, Cyrillic, and CJK words when stripping markdown, while still removing standalone `_italic_` markers on the shared text-runtime path used by LINE and TTS. (#47465) Thanks @jackjin1997. - TTS/Microsoft: auto-switch the default Edge voice to Chinese for CJK-dominant text without overriding explicitly selected Microsoft voices. (#52355) Thanks @extrasmall0. - Agents/context pruning: count supplementary-plane CJK characters with the shared code-point-aware estimator so context pruning stops underestimating Japanese and Chinese text that uses Extension B ideographs. (#39985) Thanks @Edward-Qiang-2024. diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index 937652bdfbe..2acc16b601d 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -85,10 +85,36 @@ enum HostEnvSecurityPolicy { "PIP_INDEX_URL", "PIP_PYPI_URL", "PIP_EXTRA_INDEX_URL", + "PIP_CONFIG_FILE", + "PIP_FIND_LINKS", + "PIP_TRUSTED_HOST", "UV_INDEX", "UV_INDEX_URL", "UV_EXTRA_INDEX_URL", "UV_DEFAULT_INDEX", + "DOCKER_HOST", + "DOCKER_TLS_VERIFY", + "DOCKER_CERT_PATH", + "DOCKER_CONTEXT", + "LIBRARY_PATH", + "CPATH", + "C_INCLUDE_PATH", + "CPLUS_INCLUDE_PATH", + "OBJC_INCLUDE_PATH", + "NODE_EXTRA_CA_CERTS", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "REQUESTS_CA_BUNDLE", + "CURL_CA_BUNDLE", + "GOPROXY", + "GONOSUMCHECK", + "GONOSUMDB", + "GONOPROXY", + "GOPRIVATE", + "GOENV", + "GOPATH", + "PYTHONUSERBASE", + "VIRTUAL_ENV", "LUA_PATH", "LUA_CPATH", "GEM_HOME", diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index 4c4c71fd572..6e0dd215f25 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -78,10 +78,36 @@ "PIP_INDEX_URL", "PIP_PYPI_URL", "PIP_EXTRA_INDEX_URL", + "PIP_CONFIG_FILE", + "PIP_FIND_LINKS", + "PIP_TRUSTED_HOST", "UV_INDEX", "UV_INDEX_URL", "UV_EXTRA_INDEX_URL", "UV_DEFAULT_INDEX", + "DOCKER_HOST", + "DOCKER_TLS_VERIFY", + "DOCKER_CERT_PATH", + "DOCKER_CONTEXT", + "LIBRARY_PATH", + "CPATH", + "C_INCLUDE_PATH", + "CPLUS_INCLUDE_PATH", + "OBJC_INCLUDE_PATH", + "NODE_EXTRA_CA_CERTS", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "REQUESTS_CA_BUNDLE", + "CURL_CA_BUNDLE", + "GOPROXY", + "GONOSUMCHECK", + "GONOSUMDB", + "GONOPROXY", + "GOPRIVATE", + "GOENV", + "GOPATH", + "PYTHONUSERBASE", + "VIRTUAL_ENV", "LUA_PATH", "LUA_CPATH", "GEM_HOME", diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 85c7687dace..bb063dcb1b0 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -236,10 +236,36 @@ describe("sanitizeHostExecEnv", () => { PIP_INDEX_URL: "https://example.invalid/simple", PIP_PYPI_URL: "https://example.invalid/simple", PIP_EXTRA_INDEX_URL: "https://example.invalid/simple", + PIP_CONFIG_FILE: "/tmp/evil-pip.conf", + PIP_FIND_LINKS: "https://example.invalid/wheels", + PIP_TRUSTED_HOST: "example.invalid", UV_INDEX: "https://example.invalid/simple", UV_INDEX_URL: "https://example.invalid/simple", UV_DEFAULT_INDEX: "https://example.invalid/simple", UV_EXTRA_INDEX_URL: "https://example.invalid/simple", + DOCKER_HOST: "tcp://example.invalid:2376", + DOCKER_TLS_VERIFY: "1", + DOCKER_CERT_PATH: "/tmp/evil-docker-certs", + DOCKER_CONTEXT: "evil-remote", + LIBRARY_PATH: "/tmp/evil-lib", + CPATH: "/tmp/evil-headers", + C_INCLUDE_PATH: "/tmp/evil-c-headers", + CPLUS_INCLUDE_PATH: "/tmp/evil-cpp-headers", + OBJC_INCLUDE_PATH: "/tmp/evil-objc-headers", + NODE_EXTRA_CA_CERTS: "/tmp/evil-ca.pem", + SSL_CERT_FILE: "/tmp/evil-cert.pem", + SSL_CERT_DIR: "/tmp/evil-cert-dir", + REQUESTS_CA_BUNDLE: "/tmp/evil-requests-ca.pem", + CURL_CA_BUNDLE: "/tmp/evil-curl-ca.pem", + GOPROXY: "https://example.invalid/proxy", + GONOSUMCHECK: "example.invalid/*", + GONOSUMDB: "example.invalid/*", + GONOPROXY: "example.invalid/*", + GOPRIVATE: "example.invalid/*", + GOENV: "/tmp/evil-goenv", + GOPATH: "/tmp/evil-go", + PYTHONUSERBASE: "/tmp/evil-python-userbase", + VIRTUAL_ENV: "/tmp/evil-venv", SHELLOPTS: "xtrace", PS4: "$(touch /tmp/pwned)", CLASSPATH: "/tmp/evil-classpath", @@ -277,10 +303,36 @@ describe("sanitizeHostExecEnv", () => { expect(env.PIP_INDEX_URL).toBeUndefined(); expect(env.PIP_PYPI_URL).toBeUndefined(); expect(env.PIP_EXTRA_INDEX_URL).toBeUndefined(); + expect(env.PIP_CONFIG_FILE).toBeUndefined(); + expect(env.PIP_FIND_LINKS).toBeUndefined(); + expect(env.PIP_TRUSTED_HOST).toBeUndefined(); expect(env.UV_INDEX).toBeUndefined(); expect(env.UV_INDEX_URL).toBeUndefined(); expect(env.UV_DEFAULT_INDEX).toBeUndefined(); expect(env.UV_EXTRA_INDEX_URL).toBeUndefined(); + expect(env.DOCKER_HOST).toBeUndefined(); + expect(env.DOCKER_TLS_VERIFY).toBeUndefined(); + expect(env.DOCKER_CERT_PATH).toBeUndefined(); + expect(env.DOCKER_CONTEXT).toBeUndefined(); + expect(env.LIBRARY_PATH).toBeUndefined(); + expect(env.CPATH).toBeUndefined(); + expect(env.C_INCLUDE_PATH).toBeUndefined(); + expect(env.CPLUS_INCLUDE_PATH).toBeUndefined(); + expect(env.OBJC_INCLUDE_PATH).toBeUndefined(); + expect(env.NODE_EXTRA_CA_CERTS).toBeUndefined(); + expect(env.SSL_CERT_FILE).toBeUndefined(); + expect(env.SSL_CERT_DIR).toBeUndefined(); + expect(env.REQUESTS_CA_BUNDLE).toBeUndefined(); + expect(env.CURL_CA_BUNDLE).toBeUndefined(); + expect(env.GOPROXY).toBeUndefined(); + expect(env.GONOSUMCHECK).toBeUndefined(); + expect(env.GONOSUMDB).toBeUndefined(); + expect(env.GONOPROXY).toBeUndefined(); + expect(env.GOPRIVATE).toBeUndefined(); + expect(env.GOENV).toBeUndefined(); + expect(env.GOPATH).toBeUndefined(); + expect(env.PYTHONUSERBASE).toBeUndefined(); + expect(env.VIRTUAL_ENV).toBeUndefined(); expect(env.SAFE).toBe("ok"); expect(env.HOME).toBe("/tmp/trusted-home"); expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir"); @@ -369,12 +421,29 @@ describe("isDangerousHostEnvOverrideVarName", () => { expect(isDangerousHostEnvOverrideVarName("GRADLE_USER_HOME")).toBe(true); expect(isDangerousHostEnvOverrideVarName("gradle_user_home")).toBe(true); expect(isDangerousHostEnvOverrideVarName("PIP_INDEX_URL")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("pip_config_file")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("PIP_FIND_LINKS")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("pip_trusted_host")).toBe(true); expect(isDangerousHostEnvOverrideVarName("pip_pypi_url")).toBe(true); expect(isDangerousHostEnvOverrideVarName("PIP_EXTRA_INDEX_URL")).toBe(true); expect(isDangerousHostEnvOverrideVarName("UV_INDEX")).toBe(true); expect(isDangerousHostEnvOverrideVarName("UV_INDEX_URL")).toBe(true); expect(isDangerousHostEnvOverrideVarName("uv_default_index")).toBe(true); expect(isDangerousHostEnvOverrideVarName("UV_EXTRA_INDEX_URL")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("DOCKER_HOST")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("docker_context")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("NODE_EXTRA_CA_CERTS")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("ssl_cert_file")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("REQUESTS_CA_BUNDLE")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("curl_ca_bundle")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("LIBRARY_PATH")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("c_include_path")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("GOPROXY")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("gonosumdb")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("GOPRIVATE")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("goenv")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("PYTHONUSERBASE")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("virtual_env")).toBe(true); expect(isDangerousHostEnvOverrideVarName("CLASSPATH")).toBe(true); expect(isDangerousHostEnvOverrideVarName("classpath")).toBe(true); expect(isDangerousHostEnvOverrideVarName("GOFLAGS")).toBe(true); @@ -404,27 +473,79 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { PIP_INDEX_URL: "https://example.invalid/simple", PIP_PYPI_URL: "https://example.invalid/simple", PIP_EXTRA_INDEX_URL: "https://example.invalid/simple", + PIP_CONFIG_FILE: "/tmp/evil-pip.conf", + PIP_FIND_LINKS: "https://example.invalid/wheels", + PIP_TRUSTED_HOST: "example.invalid", UV_INDEX: "https://example.invalid/simple", UV_INDEX_URL: "https://example.invalid/simple", UV_DEFAULT_INDEX: "https://example.invalid/simple", UV_EXTRA_INDEX_URL: "https://example.invalid/simple", + DOCKER_HOST: "tcp://example.invalid:2376", + DOCKER_TLS_VERIFY: "1", + DOCKER_CERT_PATH: "/tmp/evil-docker-certs", + DOCKER_CONTEXT: "evil-remote", + LIBRARY_PATH: "/tmp/evil-lib", + CPATH: "/tmp/evil-headers", + C_INCLUDE_PATH: "/tmp/evil-c-headers", + CPLUS_INCLUDE_PATH: "/tmp/evil-cpp-headers", + OBJC_INCLUDE_PATH: "/tmp/evil-objc-headers", + NODE_EXTRA_CA_CERTS: "/tmp/evil-ca.pem", + SSL_CERT_FILE: "/tmp/evil-cert.pem", + SSL_CERT_DIR: "/tmp/evil-cert-dir", + REQUESTS_CA_BUNDLE: "/tmp/evil-requests-ca.pem", + CURL_CA_BUNDLE: "/tmp/evil-curl-ca.pem", + GOPROXY: "https://example.invalid/proxy", + GONOSUMCHECK: "example.invalid/*", + GONOSUMDB: "example.invalid/*", + GONOPROXY: "example.invalid/*", + GOPRIVATE: "example.invalid/*", + GOENV: "/tmp/evil-goenv", + GOPATH: "/tmp/evil-go", + PYTHONUSERBASE: "/tmp/evil-python-userbase", + VIRTUAL_ENV: "/tmp/evil-venv", SAFE_KEY: "ok", "BAD-KEY": "bad", }, }); expect(result.rejectedOverrideBlockedKeys).toEqual([ + "C_INCLUDE_PATH", "CLASSPATH", "CMAKE_C_COMPILER", + "CPATH", + "CPLUS_INCLUDE_PATH", + "CURL_CA_BUNDLE", "CXX", + "DOCKER_CERT_PATH", + "DOCKER_CONTEXT", + "DOCKER_HOST", + "DOCKER_TLS_VERIFY", + "GOENV", + "GONOPROXY", + "GONOSUMCHECK", + "GONOSUMDB", + "GOPATH", + "GOPRIVATE", + "GOPROXY", + "LIBRARY_PATH", + "NODE_EXTRA_CA_CERTS", + "OBJC_INCLUDE_PATH", "PATH", + "PIP_CONFIG_FILE", "PIP_EXTRA_INDEX_URL", + "PIP_FIND_LINKS", "PIP_INDEX_URL", "PIP_PYPI_URL", + "PIP_TRUSTED_HOST", + "PYTHONUSERBASE", + "REQUESTS_CA_BUNDLE", + "SSL_CERT_DIR", + "SSL_CERT_FILE", "UV_DEFAULT_INDEX", "UV_EXTRA_INDEX_URL", "UV_INDEX", "UV_INDEX_URL", + "VIRTUAL_ENV", ]); expect(result.rejectedOverrideInvalidKeys).toEqual(["BAD-KEY"]); expect(result.env.SAFE_KEY).toBe("ok"); @@ -435,10 +556,36 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.PIP_INDEX_URL).toBeUndefined(); expect(result.env.PIP_PYPI_URL).toBeUndefined(); expect(result.env.PIP_EXTRA_INDEX_URL).toBeUndefined(); + expect(result.env.PIP_CONFIG_FILE).toBeUndefined(); + expect(result.env.PIP_FIND_LINKS).toBeUndefined(); + expect(result.env.PIP_TRUSTED_HOST).toBeUndefined(); expect(result.env.UV_INDEX).toBeUndefined(); expect(result.env.UV_INDEX_URL).toBeUndefined(); expect(result.env.UV_DEFAULT_INDEX).toBeUndefined(); expect(result.env.UV_EXTRA_INDEX_URL).toBeUndefined(); + expect(result.env.DOCKER_HOST).toBeUndefined(); + expect(result.env.DOCKER_TLS_VERIFY).toBeUndefined(); + expect(result.env.DOCKER_CERT_PATH).toBeUndefined(); + expect(result.env.DOCKER_CONTEXT).toBeUndefined(); + expect(result.env.LIBRARY_PATH).toBeUndefined(); + expect(result.env.CPATH).toBeUndefined(); + expect(result.env.C_INCLUDE_PATH).toBeUndefined(); + expect(result.env.CPLUS_INCLUDE_PATH).toBeUndefined(); + expect(result.env.OBJC_INCLUDE_PATH).toBeUndefined(); + expect(result.env.NODE_EXTRA_CA_CERTS).toBeUndefined(); + expect(result.env.SSL_CERT_FILE).toBeUndefined(); + expect(result.env.SSL_CERT_DIR).toBeUndefined(); + expect(result.env.REQUESTS_CA_BUNDLE).toBeUndefined(); + expect(result.env.CURL_CA_BUNDLE).toBeUndefined(); + expect(result.env.GOPROXY).toBeUndefined(); + expect(result.env.GONOSUMCHECK).toBeUndefined(); + expect(result.env.GONOSUMDB).toBeUndefined(); + expect(result.env.GONOPROXY).toBeUndefined(); + expect(result.env.GOPRIVATE).toBeUndefined(); + expect(result.env.GOENV).toBeUndefined(); + expect(result.env.GOPATH).toBeUndefined(); + expect(result.env.PYTHONUSERBASE).toBeUndefined(); + expect(result.env.VIRTUAL_ENV).toBeUndefined(); }); it("allows Windows-style override names while still rejecting invalid keys", () => {