diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9a09ff23d..bc3070d1534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ Docs: https://docs.openclaw.ai - Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk. - iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman. - Git/env sanitization: block additional Git repository-plumbing env variables such as `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, and `GIT_NAMESPACE` so host-run Git commands cannot be redirected to attacker-chosen repository state through inherited or request-scoped env. (#62002) Thanks @eleqtrizit. +- Host exec/env sanitization: block additional request-scoped credential and config-path overrides such as `KUBECONFIG`, cloud credential-path env, `CARGO_HOME`, and `HELM_HOME` so host-run tools can no longer be redirected to attacker-chosen config or state. (#59119) Thanks @eleqtrizit. ## 2026.4.5 diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index cddda7bce40..8e9be035a08 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -6,182 +6,180 @@ import Foundation enum HostEnvSecurityPolicy { static let blockedKeys: Set = [ - "NODE_OPTIONS", - "NODE_PATH", - "PYTHONHOME", - "PYTHONPATH", - "PERL5LIB", - "PERL5OPT", - "RUBYLIB", - "RUBYOPT", + "_JAVA_OPTIONS", + "ANT_OPTS", "BASH_ENV", - "ENV", "BROWSER", - "GIT_EDITOR", - "GIT_EXTERNAL_DIFF", - "GIT_DIR", - "GIT_WORK_TREE", - "GIT_COMMON_DIR", - "GIT_EXEC_PATH", - "GIT_INDEX_FILE", - "GIT_OBJECT_DIRECTORY", - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - "GIT_NAMESPACE", - "GIT_SEQUENCE_EDITOR", - "GIT_TEMPLATE_DIR", - "GIT_SSL_NO_VERIFY", - "GIT_SSL_CAINFO", - "GIT_SSL_CAPATH", - "CC", - "CXX", "CARGO_BUILD_RUSTC", "CARGO_BUILD_RUSTC_WRAPPER", - "RUSTC_WRAPPER", + "CC", "CMAKE_C_COMPILER", "CMAKE_CXX_COMPILER", - "SHELL", - "SHELLOPTS", - "PS4", + "CXX", + "DOTNET_ADDITIONAL_DEPS", + "DOTNET_STARTUP_HOOKS", + "ENV", "GCONV_PATH", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_DIR", + "GIT_EDITOR", + "GIT_EXEC_PATH", + "GIT_EXTERNAL_DIFF", + "GIT_INDEX_FILE", + "GIT_NAMESPACE", + "GIT_OBJECT_DIRECTORY", + "GIT_SEQUENCE_EDITOR", + "GIT_SSL_CAINFO", + "GIT_SSL_CAPATH", + "GIT_SSL_NO_VERIFY", + "GIT_TEMPLATE_DIR", + "GIT_WORK_TREE", + "GLIBC_TUNABLES", + "GRADLE_OPTS", + "HGRCPATH", "IFS", - "SSLKEYLOGFILE", "JAVA_OPTS", "JAVA_TOOL_OPTIONS", - "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS", - "PYTHONBREAKPOINT", - "DOTNET_STARTUP_HOOKS", - "DOTNET_ADDITIONAL_DEPS", - "GLIBC_TUNABLES", - "MAVEN_OPTS", "MAKEFLAGS", + "MAVEN_OPTS", "MFLAGS", + "NODE_OPTIONS", + "NODE_PATH", + "PERL5LIB", + "PERL5OPT", + "PS4", + "PYTHONBREAKPOINT", + "PYTHONHOME", + "PYTHONPATH", + "RUBYLIB", + "RUBYOPT", + "RUSTC_WRAPPER", "SBT_OPTS", - "GRADLE_OPTS", - "ANT_OPTS", - "HGRCPATH" + "SHELL", + "SHELLOPTS", + "SSLKEYLOGFILE" ] static let blockedOverrideKeys: Set = [ - "HOME", - "GRADLE_USER_HOME", - "ZDOTDIR", - "GIT_DIR", - "GIT_WORK_TREE", - "GIT_COMMON_DIR", - "GIT_INDEX_FILE", - "GIT_OBJECT_DIRECTORY", - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - "GIT_NAMESPACE", - "GIT_SSH_COMMAND", - "GIT_SSH", - "GIT_PROXY_COMMAND", - "GIT_ASKPASS", - "GIT_SSL_NO_VERIFY", - "GIT_SSL_CAINFO", - "GIT_SSL_CAPATH", - "SSH_ASKPASS", - "LESSOPEN", - "LESSCLOSE", - "PAGER", - "MANPAGER", - "GIT_PAGER", - "EDITOR", - "VISUAL", - "FCEDIT", - "SUDO_EDITOR", - "PROMPT_COMMAND", - "HISTFILE", - "PERL5DB", - "PERL5DBCMD", - "OPENSSL_CONF", - "OPENSSL_ENGINES", - "PYTHONSTARTUP", - "WGETRC", - "CURL_HOME", - "CLASSPATH", + "ALL_PROXY", + "AWS_CONFIG_FILE", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AZURE_AUTH_LOCATION", + "BUN_CONFIG_REGISTRY", + "BUNDLE_GEMFILE", + "C_INCLUDE_PATH", + "CARGO_BUILD_RUSTC_WRAPPER", + "CARGO_HOME", "CGO_CFLAGS", "CGO_LDFLAGS", - "GOFLAGS", - "MAKEFLAGS", - "MFLAGS", + "CLASSPATH", + "COMPOSER_HOME", "CORECLR_PROFILER_PATH", - "PHPRC", - "PHP_INI_SCAN_DIR", - "DENO_DIR", - "BUN_CONFIG_REGISTRY", - "YARN_RC_FILENAME", - "HTTP_PROXY", - "HTTPS_PROXY", - "ALL_PROXY", - "NO_PROXY", - "NODE_TLS_REJECT_UNAUTHORIZED", - "NODE_EXTRA_CA_CERTS", - "SSL_CERT_FILE", - "SSL_CERT_DIR", - "REQUESTS_CA_BUNDLE", + "CPATH", + "CPLUS_INCLUDE_PATH", "CURL_CA_BUNDLE", + "CURL_HOME", + "DENO_DIR", + "DOCKER_CERT_PATH", + "DOCKER_CONTEXT", "DOCKER_HOST", "DOCKER_TLS_VERIFY", - "DOCKER_CERT_PATH", + "EDITOR", + "FCEDIT", + "GEM_HOME", + "GEM_PATH", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_ASKPASS", + "GIT_COMMON_DIR", + "GIT_DIR", + "GIT_INDEX_FILE", + "GIT_NAMESPACE", + "GIT_OBJECT_DIRECTORY", + "GIT_PAGER", + "GIT_PROXY_COMMAND", + "GIT_SSH", + "GIT_SSH_COMMAND", + "GIT_SSL_CAINFO", + "GIT_SSL_CAPATH", + "GIT_SSL_NO_VERIFY", + "GIT_WORK_TREE", + "GOENV", + "GOFLAGS", + "GONOPROXY", + "GONOSUMCHECK", + "GONOSUMDB", + "GOOGLE_APPLICATION_CREDENTIALS", + "GOPATH", + "GOPRIVATE", + "GOPROXY", + "GRADLE_USER_HOME", + "HELM_HOME", + "HGRCPATH", + "HISTFILE", + "HOME", + "HTTP_PROXY", + "HTTPS_PROXY", + "KUBECONFIG", + "LESSCLOSE", + "LESSOPEN", + "LIBRARY_PATH", + "LUA_CPATH", + "LUA_PATH", + "MAKEFLAGS", + "MANPAGER", + "MFLAGS", + "NO_PROXY", + "NODE_EXTRA_CA_CERTS", + "NODE_TLS_REJECT_UNAUTHORIZED", + "OBJC_INCLUDE_PATH", + "OPENSSL_CONF", + "OPENSSL_ENGINES", + "PAGER", + "PERL5DB", + "PERL5DBCMD", + "PHP_INI_SCAN_DIR", + "PHPRC", + "PIP_CONFIG_FILE", + "PIP_EXTRA_INDEX_URL", + "PIP_FIND_LINKS", "PIP_INDEX_URL", "PIP_PYPI_URL", - "PIP_EXTRA_INDEX_URL", - "PIP_CONFIG_FILE", - "PIP_FIND_LINKS", "PIP_TRUSTED_HOST", + "PROMPT_COMMAND", + "PYTHONSTARTUP", + "PYTHONUSERBASE", + "REQUESTS_CA_BUNDLE", + "RUSTC_WRAPPER", + "RUSTFLAGS", + "SSH_ASKPASS", + "SSL_CERT_DIR", + "SSL_CERT_FILE", + "SUDO_EDITOR", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", "UV_INDEX", "UV_INDEX_URL", "UV_PYTHON", - "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", - "HGRCPATH", - "PYTHONUSERBASE", - "RUSTC_WRAPPER", - "RUSTFLAGS", - "CARGO_HOME", "VIRTUAL_ENV", - "LUA_PATH", - "LUA_CPATH", - "GEM_HOME", - "GEM_PATH", - "BUNDLE_GEMFILE", - "COMPOSER_HOME", - "CARGO_BUILD_RUSTC_WRAPPER", + "VISUAL", + "WGETRC", "XDG_CONFIG_HOME", - "AWS_CONFIG_FILE" + "YARN_RC_FILENAME", + "ZDOTDIR" ] static let blockedOverridePrefixes: [String] = [ + "CARGO_REGISTRIES_", "GIT_CONFIG_", - "NPM_CONFIG_", - "CARGO_REGISTRIES_" + "NPM_CONFIG_" ] static let blockedPrefixes: [String] = [ + "BASH_FUNC_", "DYLD_", - "LD_", - "BASH_FUNC_" + "LD_" ] } diff --git a/scripts/generate-host-env-security-policy-swift.mjs b/scripts/generate-host-env-security-policy-swift.mjs index b87966c491e..e31c2b83838 100644 --- a/scripts/generate-host-env-security-policy-swift.mjs +++ b/scripts/generate-host-env-security-policy-swift.mjs @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { loadHostEnvSecurityPolicy } from "../src/infra/host-env-security-policy.js"; const args = new Set(process.argv.slice(2)); const checkOnly = args.has("--check"); @@ -24,8 +25,8 @@ const outputPath = path.join( "HostEnvSecurityPolicy.generated.swift", ); -/** @type {{blockedKeys: string[]; blockedOverrideKeys?: string[]; blockedOverridePrefixes?: string[]; blockedPrefixes: string[]}} */ -const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")); +const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8")); +const policy = loadHostEnvSecurityPolicy(rawPolicy); const renderSwiftStringArray = (items) => items.map((item) => ` "${item}"`).join(",\n"); diff --git a/src/infra/host-env-security-policy.d.ts b/src/infra/host-env-security-policy.d.ts new file mode 100644 index 00000000000..f9b18d43a70 --- /dev/null +++ b/src/infra/host-env-security-policy.d.ts @@ -0,0 +1,14 @@ +export type HostEnvSecurityPolicy = Readonly<{ + blockedEverywhereKeys: readonly string[]; + blockedOverrideOnlyKeys: readonly string[]; + blockedPrefixes: readonly string[]; + blockedOverridePrefixes: readonly string[]; + blockedKeys: readonly string[]; + blockedOverrideKeys: readonly string[]; +}>; + +export declare function loadHostEnvSecurityPolicy( + rawPolicy?: Partial, +): HostEnvSecurityPolicy; + +export declare const HOST_ENV_SECURITY_POLICY: HostEnvSecurityPolicy; diff --git a/src/infra/host-env-security-policy.js b/src/infra/host-env-security-policy.js new file mode 100644 index 00000000000..cd2dadd7c86 --- /dev/null +++ b/src/infra/host-env-security-policy.js @@ -0,0 +1,35 @@ +import HOST_ENV_SECURITY_POLICY_JSON from "./host-env-security-policy.json" with { type: "json" }; + +function sortUniqueUppercase(values) { + return Object.freeze( + Array.from(new Set(values.map((value) => value.toUpperCase()))).toSorted((a, b) => + a.localeCompare(b), + ), + ); +} + +function derivePolicyArrays(policy) { + const blockedEverywhereKeys = policy.blockedEverywhereKeys ?? []; + const blockedOverrideOnlyKeys = policy.blockedOverrideOnlyKeys ?? []; + + return { + blockedKeys: sortUniqueUppercase(blockedEverywhereKeys), + blockedOverrideKeys: sortUniqueUppercase(blockedOverrideOnlyKeys), + blockedPrefixes: sortUniqueUppercase(policy.blockedPrefixes ?? []), + blockedOverridePrefixes: sortUniqueUppercase(policy.blockedOverridePrefixes ?? []), + }; +} + +export function loadHostEnvSecurityPolicy(rawPolicy = HOST_ENV_SECURITY_POLICY_JSON) { + const derived = derivePolicyArrays(rawPolicy); + return Object.freeze({ + blockedEverywhereKeys: Object.freeze(rawPolicy.blockedEverywhereKeys ?? []), + blockedOverrideOnlyKeys: Object.freeze(rawPolicy.blockedOverrideOnlyKeys ?? []), + blockedPrefixes: derived.blockedPrefixes, + blockedOverridePrefixes: derived.blockedOverridePrefixes, + blockedKeys: derived.blockedKeys, + blockedOverrideKeys: derived.blockedOverrideKeys, + }); +} + +export const HOST_ENV_SECURITY_POLICY = loadHostEnvSecurityPolicy(); diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index acb654e8b51..9ead4b98220 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -1,5 +1,5 @@ { - "blockedKeys": [ + "blockedEverywhereKeys": [ "NODE_OPTIONS", "NODE_PATH", "PYTHONHOME", @@ -55,7 +55,7 @@ "ANT_OPTS", "HGRCPATH" ], - "blockedOverrideKeys": [ + "blockedOverrideOnlyKeys": [ "HOME", "GRADLE_USER_HOME", "ZDOTDIR", @@ -128,9 +128,6 @@ "UV_PYTHON", "UV_EXTRA_INDEX_URL", "UV_DEFAULT_INDEX", - "DOCKER_HOST", - "DOCKER_TLS_VERIFY", - "DOCKER_CERT_PATH", "DOCKER_CONTEXT", "LIBRARY_PATH", "CPATH", @@ -163,7 +160,14 @@ "COMPOSER_HOME", "CARGO_BUILD_RUSTC_WRAPPER", "XDG_CONFIG_HOME", - "AWS_CONFIG_FILE" + "AWS_CONFIG_FILE", + "KUBECONFIG", + "GOOGLE_APPLICATION_CREDENTIALS", + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AZURE_AUTH_LOCATION", + "CARGO_HOME", + "HELM_HOME" ], "blockedOverridePrefixes": ["GIT_CONFIG_", "NPM_CONFIG_", "CARGO_REGISTRIES_"], "blockedPrefixes": ["DYLD_", "LD_", "BASH_FUNC_"] diff --git a/src/infra/host-env-security.policy-parity.test.ts b/src/infra/host-env-security.policy-parity.test.ts index 8ed1990e803..295d1c5e1fe 100644 --- a/src/infra/host-env-security.policy-parity.test.ts +++ b/src/infra/host-env-security.policy-parity.test.ts @@ -1,13 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; - -type HostEnvSecurityPolicy = { - blockedKeys: string[]; - blockedOverrideKeys?: string[]; - blockedOverridePrefixes?: string[]; - blockedPrefixes: string[]; -}; +import { loadHostEnvSecurityPolicy } from "./host-env-security-policy.js"; function parseSwiftStringArray(source: string, marker: string): string[] { const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -19,6 +13,10 @@ function parseSwiftStringArray(source: string, marker: string): string[] { return Array.from(match[1].matchAll(/"([^"]+)"/g), (m) => m[1]); } +function sortUnique(values: string[]): string[] { + return Array.from(new Set(values)).toSorted((a, b) => a.localeCompare(b)); +} + describe("host env security policy parity", () => { it("keeps generated macOS host env policy in sync with shared JSON policy", () => { const repoRoot = process.cwd(); @@ -32,7 +30,8 @@ describe("host env security policy parity", () => { "apps/macos/Sources/OpenClaw/HostEnvSanitizer.swift", ); - const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")) as HostEnvSecurityPolicy; + const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8")); + const policy = loadHostEnvSecurityPolicy(rawPolicy); const generatedSource = fs.readFileSync(generatedSwiftPath, "utf8"); const sanitizerSource = fs.readFileSync(sanitizerSwiftPath, "utf8"); @@ -68,4 +67,14 @@ describe("host env security policy parity", () => { "private static let blockedPrefixes = HostEnvSecurityPolicy.blockedPrefixes", ); }); + + it("derives inherited and override lists from explicit policy buckets", () => { + const repoRoot = process.cwd(); + const policyPath = path.join(repoRoot, "src/infra/host-env-security-policy.json"); + const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8")); + const policy = loadHostEnvSecurityPolicy(rawPolicy); + + expect(policy.blockedKeys).toEqual(sortUnique([...policy.blockedEverywhereKeys])); + expect(policy.blockedOverrideKeys).toEqual(sortUnique([...policy.blockedOverrideOnlyKeys])); + }); }); diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index f235eb36f4b..08b7de1b5e6 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -146,18 +146,25 @@ describe("isDangerousHostEnvVarName", () => { expect(isDangerousHostEnvVarName("git_sequence_editor")).toBe(true); expect(isDangerousHostEnvVarName("GIT_TEMPLATE_DIR")).toBe(true); expect(isDangerousHostEnvVarName("git_template_dir")).toBe(true); + expect(isDangerousHostEnvVarName("KUBECONFIG")).toBe(false); + expect(isDangerousHostEnvVarName("google_application_credentials")).toBe(false); + expect(isDangerousHostEnvVarName("AWS_SHARED_CREDENTIALS_FILE")).toBe(false); + expect(isDangerousHostEnvVarName("aws_web_identity_token_file")).toBe(false); + expect(isDangerousHostEnvVarName("AZURE_AUTH_LOCATION")).toBe(false); expect(isDangerousHostEnvVarName("CC")).toBe(true); 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("cargo_home")).toBe(false); 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("HELM_HOME")).toBe(false); expect(isDangerousHostEnvVarName("SHELLOPTS")).toBe(true); expect(isDangerousHostEnvVarName("ps4")).toBe(true); expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true); @@ -242,7 +249,19 @@ describe("sanitizeHostExecEnv", () => { JAVA_OPTS: "-javaagent:/tmp/evil.jar", MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", + KUBECONFIG: "/tmp/kubeconfig", + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp.json", + AWS_SHARED_CREDENTIALS_FILE: "/tmp/aws-credentials", + AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/aws-web-token", + AZURE_AUTH_LOCATION: "/tmp/azure-auth.json", AWS_CONFIG_FILE: "/tmp/aws-config", + CARGO_HOME: "/tmp/cargo", + HELM_HOME: "/tmp/helm", + HTTP_PROXY: "http://proxy.example.test:8080", + HTTPS_PROXY: "http://proxy.example.test:8443", + SSL_CERT_FILE: "/tmp/evil-cert.pem", + SSL_CERT_DIR: "/tmp/evil-cert-dir", + DOCKER_HOST: "tcp://docker.example.test:2376", LD_PRELOAD: "/tmp/pwn.so", OK: "1", }, @@ -252,6 +271,18 @@ describe("sanitizeHostExecEnv", () => { OPENCLAW_CLI: OPENCLAW_CLI_ENV_VALUE, PATH: "/usr/bin:/bin", AWS_CONFIG_FILE: "/tmp/aws-config", + KUBECONFIG: "/tmp/kubeconfig", + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp.json", + AWS_SHARED_CREDENTIALS_FILE: "/tmp/aws-credentials", + AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/aws-web-token", + AZURE_AUTH_LOCATION: "/tmp/azure-auth.json", + CARGO_HOME: "/tmp/cargo", + HELM_HOME: "/tmp/helm", + HTTP_PROXY: "http://proxy.example.test:8080", + HTTPS_PROXY: "http://proxy.example.test:8443", + SSL_CERT_FILE: "/tmp/evil-cert.pem", + SSL_CERT_DIR: "/tmp/evil-cert-dir", + DOCKER_HOST: "tcp://docker.example.test:2376", OK: "1", }); }); @@ -296,6 +327,11 @@ describe("sanitizeHostExecEnv", () => { CARGO_REGISTRIES_CRATES_IO_INDEX: "https://example.invalid/crates.io-index", AWS_CONFIG_FILE: "/tmp/override-aws-config", YARN_RC_FILENAME: ".evil-yarnrc.yml", + KUBECONFIG: "/tmp/override-kubeconfig", + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/override-gcp.json", + AWS_SHARED_CREDENTIALS_FILE: "/tmp/override-aws-credentials", + AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/override-aws-web-token", + AZURE_AUTH_LOCATION: "/tmp/override-azure-auth.json", PIP_INDEX_URL: "https://example.invalid/simple", PIP_PYPI_URL: "https://example.invalid/simple", PIP_EXTRA_INDEX_URL: "https://example.invalid/simple", @@ -316,6 +352,8 @@ describe("sanitizeHostExecEnv", () => { C_INCLUDE_PATH: "/tmp/evil-c-headers", CPLUS_INCLUDE_PATH: "/tmp/evil-cpp-headers", OBJC_INCLUDE_PATH: "/tmp/evil-objc-headers", + CARGO_HOME: "/tmp/override-cargo", + HELM_HOME: "/tmp/override-helm", NODE_EXTRA_CA_CERTS: "/tmp/evil-ca.pem", SSL_CERT_FILE: "/tmp/evil-cert.pem", SSL_CERT_DIR: "/tmp/evil-cert-dir", @@ -371,6 +409,11 @@ describe("sanitizeHostExecEnv", () => { expect(env.GIT_NAMESPACE).toBeUndefined(); expect(env.GIT_SEQUENCE_EDITOR).toBeUndefined(); expect(env.AWS_CONFIG_FILE).toBeUndefined(); + expect(env.KUBECONFIG).toBeUndefined(); + expect(env.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined(); + expect(env.AWS_SHARED_CREDENTIALS_FILE).toBeUndefined(); + expect(env.AWS_WEB_IDENTITY_TOKEN_FILE).toBeUndefined(); + expect(env.AZURE_AUTH_LOCATION).toBeUndefined(); expect(env.GIT_SSH_COMMAND).toBeUndefined(); expect(env.GIT_EXEC_PATH).toBeUndefined(); expect(env.EDITOR).toBeUndefined(); @@ -421,6 +464,7 @@ describe("sanitizeHostExecEnv", () => { expect(env.GOENV).toBeUndefined(); expect(env.GOPATH).toBeUndefined(); expect(env.CARGO_HOME).toBeUndefined(); + expect(env.HELM_HOME).toBeUndefined(); expect(env.PYTHONUSERBASE).toBeUndefined(); expect(env.VIRTUAL_ENV).toBeUndefined(); expect(env.SAFE).toBe("ok"); @@ -428,7 +472,7 @@ describe("sanitizeHostExecEnv", () => { expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir"); }); - it("keeps trusted inherited proxy, TLS, and Docker env while blocking overrides", () => { + it("keeps trusted inherited proxy and TLS env while blocking overrides", () => { const env = sanitizeHostExecEnv({ baseEnv: { PATH: "/usr/bin:/bin", @@ -591,6 +635,13 @@ describe("isDangerousHostEnvOverrideVarName", () => { expect(isDangerousHostEnvOverrideVarName("goenv")).toBe(true); expect(isDangerousHostEnvOverrideVarName("PYTHONUSERBASE")).toBe(true); expect(isDangerousHostEnvOverrideVarName("virtual_env")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("KUBECONFIG")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("google_application_credentials")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("AWS_SHARED_CREDENTIALS_FILE")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("aws_web_identity_token_file")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("AZURE_AUTH_LOCATION")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("cargo_home")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("HELM_HOME")).toBe(true); expect(isDangerousHostEnvOverrideVarName("CLASSPATH")).toBe(true); expect(isDangerousHostEnvOverrideVarName("classpath")).toBe(true); expect(isDangerousHostEnvOverrideVarName("MAKEFLAGS")).toBe(true); @@ -633,6 +684,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { 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", + KUBECONFIG: "/tmp/evil-kubeconfig", + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/evil-gcp.json", + AWS_SHARED_CREDENTIALS_FILE: "/tmp/evil-aws-credentials", + AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/evil-aws-web-token", + AZURE_AUTH_LOCATION: "/tmp/evil-azure-auth.json", CLASSPATH: "/tmp/evil-classpath", PIP_INDEX_URL: "https://example.invalid/simple", PIP_PYPI_URL: "https://example.invalid/simple", @@ -677,6 +733,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { HGRCPATH: "/tmp/evil-hgrc", MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", + HELM_HOME: "/tmp/evil-helm", PYTHONUSERBASE: "/tmp/evil-python-userbase", RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", RUSTFLAGS: "-C link-args=-l/tmp/evil.so", @@ -694,6 +751,9 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { }); expect(result.rejectedOverrideBlockedKeys).toEqual([ + "AWS_SHARED_CREDENTIALS_FILE", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AZURE_AUTH_LOCATION", "C_INCLUDE_PATH", "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_HOME", @@ -722,12 +782,15 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { "GONOPROXY", "GONOSUMCHECK", "GONOSUMDB", + "GOOGLE_APPLICATION_CREDENTIALS", "GOPATH", "GOPRIVATE", "GOPROXY", + "HELM_HOME", "HGRCPATH", "HTTPS_PROXY", "JAVA_OPTS", + "KUBECONFIG", "LIBRARY_PATH", "MAKEFLAGS", "MFLAGS", @@ -774,6 +837,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.UV_PYTHON).toBeUndefined(); expect(result.env.UV_DEFAULT_INDEX).toBeUndefined(); expect(result.env.UV_EXTRA_INDEX_URL).toBeUndefined(); + expect(result.env.KUBECONFIG).toBeUndefined(); + expect(result.env.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined(); + expect(result.env.AWS_SHARED_CREDENTIALS_FILE).toBeUndefined(); + expect(result.env.AWS_WEB_IDENTITY_TOKEN_FILE).toBeUndefined(); + expect(result.env.AZURE_AUTH_LOCATION).toBeUndefined(); expect(result.env.GIT_SSL_NO_VERIFY).toBeUndefined(); expect(result.env.GIT_SSL_CAINFO).toBeUndefined(); expect(result.env.GIT_SSL_CAPATH).toBeUndefined(); @@ -807,6 +875,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.GOPATH).toBeUndefined(); expect(result.env.CARGO_HOME).toBeUndefined(); expect(result.env.HGRCPATH).toBeUndefined(); + expect(result.env.HELM_HOME).toBeUndefined(); expect(result.env.HTTPS_PROXY).toBeUndefined(); expect(result.env.JAVA_OPTS).toBeUndefined(); expect(result.env.MAKEFLAGS).toBeUndefined(); diff --git a/src/infra/host-env-security.ts b/src/infra/host-env-security.ts index 83ed5f43790..8d82bdc76a9 100644 --- a/src/infra/host-env-security.ts +++ b/src/infra/host-env-security.ts @@ -1,30 +1,21 @@ -import HOST_ENV_SECURITY_POLICY_JSON from "./host-env-security-policy.json" with { type: "json" }; +import { HOST_ENV_SECURITY_POLICY } from "./host-env-security-policy.js"; import { markOpenClawExecEnv } from "./openclaw-exec-env.js"; const PORTABLE_ENV_VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/; const WINDOWS_COMPAT_OVERRIDE_ENV_VAR_KEY = /^[A-Za-z_][A-Za-z0-9_()]*$/; -type HostEnvSecurityPolicy = { - blockedKeys: string[]; - blockedOverrideKeys?: string[]; - blockedOverridePrefixes?: string[]; - blockedPrefixes: string[]; -}; - -const HOST_ENV_SECURITY_POLICY = HOST_ENV_SECURITY_POLICY_JSON as HostEnvSecurityPolicy; - -export const HOST_DANGEROUS_ENV_KEY_VALUES: readonly string[] = Object.freeze( - HOST_ENV_SECURITY_POLICY.blockedKeys.map((key) => key.toUpperCase()), -); -export const HOST_DANGEROUS_ENV_PREFIXES: readonly string[] = Object.freeze( - HOST_ENV_SECURITY_POLICY.blockedPrefixes.map((prefix) => prefix.toUpperCase()), -); -export const HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze( - (HOST_ENV_SECURITY_POLICY.blockedOverrideKeys ?? []).map((key) => key.toUpperCase()), -); -export const HOST_DANGEROUS_OVERRIDE_ENV_PREFIXES: readonly string[] = Object.freeze( - (HOST_ENV_SECURITY_POLICY.blockedOverridePrefixes ?? []).map((prefix) => prefix.toUpperCase()), -); +export const HOST_DANGEROUS_ENV_KEY_VALUES: readonly string[] = Object.freeze([ + ...HOST_ENV_SECURITY_POLICY.blockedKeys, +]); +export const HOST_DANGEROUS_ENV_PREFIXES: readonly string[] = Object.freeze([ + ...HOST_ENV_SECURITY_POLICY.blockedPrefixes, +]); +export const HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze([ + ...HOST_ENV_SECURITY_POLICY.blockedOverrideKeys, +]); +export const HOST_DANGEROUS_OVERRIDE_ENV_PREFIXES: readonly string[] = Object.freeze([ + ...HOST_ENV_SECURITY_POLICY.blockedOverridePrefixes, +]); export const HOST_SHELL_WRAPPER_ALLOWED_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze([ "TERM", "LANG",