From 9f413acc183df1edf82da1426aaebc95e47cb989 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Tue, 9 Jun 2026 19:44:54 +0530 Subject: [PATCH] fix: expand unsafe host env denylist (#91618) * fix: expand unsafe host env denylist * test: annotate host env security fixtures * test: align opengrep fixture suppressions * test: keep opengrep suppressions inline * test: avoid opengrep fixture call patterns --- .../HostEnvSecurityPolicy.generated.swift | 8 + docs/cli/mcp.md | 2 +- docs/nodes/index.md | 2 +- docs/platforms/macos.md | 2 +- src/infra/host-env-security-policy.json | 4 + .../host-env-security.reported-baseline.json | 6 +- ...ost-env-security.reported-baseline.test.ts | 2 +- src/infra/host-env-security.test.ts | 427 ++++++++++-------- 8 files changed, 252 insertions(+), 201 deletions(-) diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index 374fecd666c..408ed0476f1 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -28,6 +28,7 @@ enum HostEnvSecurityPolicy { "AWS_SESSION_TOKEN", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", + "BASHOPTS", "BASH_ENV", "BROWSER", "BUNDLE_GEMFILE", @@ -70,6 +71,7 @@ enum HostEnvSecurityPolicy { "ERL_ZFLAGS", "EXINIT", "FCEDIT", + "FPATH", "GCONV_PATH", "GEM_HOME", "GEM_PATH", @@ -116,6 +118,7 @@ enum HostEnvSecurityPolicy { "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "JULIA_EDITOR", + "KSH_ENV", "LDFLAGS", "LESSCLOSE", "LESSOPEN", @@ -183,6 +186,7 @@ enum HostEnvSecurityPolicy { "SUDO_EDITOR", "SVN_EDITOR", "SVN_SSH", + "TCLLIBPATH", "TF_CLI_CONFIG_FILE", "TF_PLUGIN_CACHE_DIR", "UV_DEFAULT_INDEX", @@ -207,6 +211,7 @@ enum HostEnvSecurityPolicy { static let blockedKeys: Set = [ "ANT_OPTS", + "BASHOPTS", "BASH_ENV", "BROWSER", "BZR_EDITOR", @@ -232,6 +237,7 @@ enum HostEnvSecurityPolicy { "ERL_FLAGS", "ERL_ZFLAGS", "EXINIT", + "FPATH", "GCONV_PATH", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", @@ -260,6 +266,7 @@ enum HostEnvSecurityPolicy { "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "JULIA_EDITOR", + "KSH_ENV", "LUA_INIT", "LUA_INIT_5_1", "LUA_INIT_5_2", @@ -297,6 +304,7 @@ enum HostEnvSecurityPolicy { "SUDO_ASKPASS", "SVN_EDITOR", "SVN_SSH", + "TCLLIBPATH", "VAGRANT_VAGRANTFILE", "VIMINIT", "_JAVA_OPTIONS" diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index 84060410560..eb287636594 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -673,7 +673,7 @@ Launches a local child process and communicates over stdin/stdout. **Stdio env safety filter** -OpenClaw rejects interpreter-startup env keys that can alter how a stdio MCP server starts up before the first RPC, even if they appear in a server's `env` block. Blocked keys include `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `SHELLOPTS`, `PS4`, and similar runtime-control variables. Startup rejects these with a configuration error so they cannot inject an implicit prelude, swap the interpreter, enable a debugger, or redirect runtime output against the stdio process. Ordinary credential, proxy, and server-specific env vars (`GITHUB_TOKEN`, `HTTP_PROXY`, custom `*_API_KEY`, etc.) are unaffected. +OpenClaw rejects interpreter-startup env keys that can alter how a stdio MCP server starts up before the first RPC, even if they appear in a server's `env` block. Blocked keys include `BASHOPTS`, `FPATH`, `KSH_ENV`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `SHELLOPTS`, `PS4`, `TCLLIBPATH`, and similar runtime-control variables. Startup rejects these with a configuration error so they cannot inject an implicit prelude, swap the interpreter, enable a debugger, or redirect runtime output against the stdio process. Ordinary credential, proxy, and server-specific env vars (`GITHUB_TOKEN`, `HTTP_PROXY`, custom `*_API_KEY`, etc.) are unaffected. If your MCP server genuinely needs one of the blocked variables, set it on the gateway host process instead of under the stdio server's `env`. diff --git a/docs/nodes/index.md b/docs/nodes/index.md index 300e29542aa..66bc013db15 100644 --- a/docs/nodes/index.md +++ b/docs/nodes/index.md @@ -381,7 +381,7 @@ Notes: - For allow-always decisions in allowlist mode, known dispatch wrappers (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically. - On Windows node hosts in allowlist mode, shell-wrapper runs via `cmd.exe /c` require approval (allowlist entry alone does not auto-allow the wrapper form). - `system.notify` supports `--priority ` and `--delivery `. -- Node hosts ignore `PATH` overrides and strip dangerous startup/shell keys (`DYLD_*`, `LD_*`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHON*`, `PERL*`, `RUBYOPT`, `SHELLOPTS`, `PS4`). If you need extra PATH entries, configure the node host service environment (or install tools in standard locations) instead of passing `PATH` via `--env`. +- Node hosts ignore `PATH` overrides and strip dangerous startup/shell keys (`DYLD_*`, `LD_*`, `BASHOPTS`, `FPATH`, `KSH_ENV`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHON*`, `PERL*`, `RUBYOPT`, `SHELLOPTS`, `PS4`, `TCLLIBPATH`). If you need extra PATH entries, configure the node host service environment (or install tools in standard locations) instead of passing `PATH` via `--env`. - On macOS node mode, `system.run` is gated by exec approvals in the macOS app (Settings → Exec approvals). Ask/allowlist/full behave the same as the headless node host; denied prompts return `SYSTEM_RUN_DENIED`. - On headless node host, `system.run` is gated by exec approvals (`~/.openclaw/exec-approvals.json`). diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index 3cea10a8f12..09cbfa0babf 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -107,7 +107,7 @@ Notes: - `allowlist` entries are glob patterns for resolved binary paths, or bare command names for PATH-invoked commands. - Raw shell command text that contains shell control or expansion syntax (`&&`, `||`, `;`, `|`, `` ` ``, `$`, `<`, `>`, `(`, `)`) is treated as an allowlist miss and requires explicit approval (or allowlisting the shell binary). - Choosing "Always Allow" in the prompt adds that command to the allowlist. -- `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHON*`, `PERL*`, `RUBYOPT`, `SHELLOPTS`, `PS4`) and then merged with the app's environment. +- `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `BASHOPTS`, `FPATH`, `KSH_ENV`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHON*`, `PERL*`, `RUBYOPT`, `SHELLOPTS`, `PS4`, `TCLLIBPATH`) and then merged with the app's environment. - For shell wrappers (`bash|sh|zsh ... -c/-lc`), request-scoped environment overrides are reduced to a small explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`). - For allow-always decisions in allowlist mode, known dispatch wrappers (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically. diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index f61aa0d1074..0ebd41af553 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -12,8 +12,10 @@ "PERL5OPT", "RUBYLIB", "RUBYOPT", + "BASHOPTS", "BASH_ENV", "ENV", + "KSH_ENV", "BROWSER", "GIT_EDITOR", "GIT_EXTERNAL_DIFF", @@ -50,6 +52,7 @@ "PYTHONBREAKPOINT", "DOTNET_STARTUP_HOOKS", "DOTNET_ADDITIONAL_DEPS", + "FPATH", "GLIBC_TUNABLES", "MAVEN_OPTS", "MAKEFLAGS", @@ -93,6 +96,7 @@ "R_PROFILE", "R_ENVIRON_USER", "R_PROFILE_USER", + "TCLLIBPATH", "HOSTALIASES" ], "blockedOverrideOnlyKeys": [ diff --git a/src/infra/host-env-security.reported-baseline.json b/src/infra/host-env-security.reported-baseline.json index 44844e3c687..0b195fbd906 100644 --- a/src/infra/host-env-security.reported-baseline.json +++ b/src/infra/host-env-security.reported-baseline.json @@ -3,6 +3,7 @@ "generatedAt": "2026-04-10", "reportedDangerousEverywhereKeys": [ "ANT_OPTS", + "BASHOPTS", "BASH_ENV", "BROWSER", "BZR_EDITOR", @@ -28,6 +29,7 @@ "ERL_FLAGS", "ERL_ZFLAGS", "EXINIT", + "FPATH", "GCONV_PATH", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", @@ -56,6 +58,7 @@ "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "JULIA_EDITOR", + "KSH_ENV", "LUA_INIT", "LUA_INIT_5_1", "LUA_INIT_5_2", @@ -93,6 +96,7 @@ "SUDO_ASKPASS", "SVN_EDITOR", "SVN_SSH", + "TCLLIBPATH", "VAGRANT_VAGRANTFILE", "VIMINIT", "_JAVA_OPTIONS" @@ -248,5 +252,5 @@ "YARN_RC_FILENAME", "ZDOTDIR" ], - "expectedTotalReportedEntries": 243 + "expectedTotalReportedEntries": 247 } diff --git a/src/infra/host-env-security.reported-baseline.test.ts b/src/infra/host-env-security.reported-baseline.test.ts index 84f277c9278..21f64ff57a7 100644 --- a/src/infra/host-env-security.reported-baseline.test.ts +++ b/src/infra/host-env-security.reported-baseline.test.ts @@ -92,7 +92,7 @@ describe("host env reported baseline coverage", () => { baseline.reportedDangerousEverywhereKeys.length + baseline.reportedDangerousOverrideOnlyKeys.length, ).toBe(baseline.expectedTotalReportedEntries); - expect(baseline.expectedTotalReportedEntries).toBe(243); + expect(baseline.expectedTotalReportedEntries).toBe(247); expect(sortUniqueUpper(baseline.reportedDangerousEverywhereKeys)).toEqual( baseline.reportedDangerousEverywhereKeys, ); diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 9e8a4da065b..e32bdab17f8 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -229,6 +229,14 @@ describe("isDangerousHostEnvVarName", () => { expect(isDangerousHostEnvVarName("AWS_CONFIG_FILE")).toBe(false); expect(isDangerousHostEnvVarName("aws_config_file")).toBe(false); expect(isDangerousHostEnvVarName("yarn_rc_filename")).toBe(false); + expect(isDangerousHostEnvVarName("BASHOPTS")).toBe(true); + expect(isDangerousHostEnvVarName("bashopts")).toBe(true); + expect(isDangerousHostEnvVarName("FPATH")).toBe(true); + expect(isDangerousHostEnvVarName("fpath")).toBe(true); + expect(isDangerousHostEnvVarName("KSH_ENV")).toBe(true); + expect(isDangerousHostEnvVarName("ksh_env")).toBe(true); + expect(isDangerousHostEnvVarName("TCLLIBPATH")).toBe(true); + expect(isDangerousHostEnvVarName("tcllibpath")).toBe(true); expect(isDangerousHostEnvVarName("PATH")).toBe(false); expect(isDangerousHostEnvVarName("FOO")).toBe(false); expect(isDangerousHostEnvVarName("GRADLE_USER_HOME")).toBe(false); @@ -337,6 +345,10 @@ describe("sanitizeHostExecEnv", () => { DOCKER_CONTEXT: "trusted-remote", DOCKER_HOST: "tcp://docker.example.test:2376", LD_PRELOAD: "/tmp/pwn.so", + BASHOPTS: "xtrace", + FPATH: "/tmp/evil-fpath", + KSH_ENV: "/tmp/evil-ksh-env", + TCLLIBPATH: "/tmp/evil-tcllibpath", NODE_REDIRECT_WARNINGS: "/tmp/node-warnings.log", NODE_REPL_EXTERNAL_MODULE: "/tmp/pwn.js", NODE_REPL_HISTORY: "/tmp/node-repl-history", @@ -366,105 +378,109 @@ describe("sanitizeHostExecEnv", () => { }); it("blocks PATH and dangerous override values", () => { - const env = sanitizeHostExecEnv({ - baseEnv: { - PATH: "/usr/bin:/bin", - HOME: "/tmp/trusted-home", - ZDOTDIR: "/tmp/trusted-zdotdir", - CARGO_REGISTRIES_CRATES_IO_INDEX: "https://trusted.example/crates.io-index", - YARN_RC_FILENAME: ".trusted-yarnrc.yml", - }, - overrides: { - PATH: "/tmp/evil", - HOME: "/tmp/evil-home", - ZDOTDIR: "/tmp/evil-zdotdir", - BASH_ENV: "/tmp/pwn.sh", - BROWSER: "/tmp/browser", - 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_DIR: "/tmp/evil-git-dir", - GIT_WORK_TREE: "/tmp/evil-work-tree", - GIT_COMMON_DIR: "/tmp/evil-common-dir", - GIT_EXEC_PATH: "/tmp/git-exec-path", - GIT_INDEX_FILE: "/tmp/evil-git-index", - GIT_OBJECT_DIRECTORY: "/tmp/evil-git-objects", - GIT_ALTERNATE_OBJECT_DIRECTORIES: "/tmp/evil-git-alt-objects", - GIT_NAMESPACE: "evil-namespace", - GIT_SEQUENCE_EDITOR: "/tmp/git-sequence-editor", - EDITOR: "/tmp/editor", - NPM_CONFIG_USERCONFIG: "/tmp/npmrc", - GIT_CONFIG_GLOBAL: "/tmp/gitconfig", - 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", - 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_PYTHON: "/tmp/evil-uv-python", - 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", - HELM_HOME: "/tmp/override-helm", - NODE_REDIRECT_WARNINGS: "/tmp/node-warnings.log", - NODE_REPL_EXTERNAL_MODULE: "/tmp/pwn.js", - NODE_REPL_HISTORY: "/tmp/node-repl-history", - NODE_V8_COVERAGE: "/tmp/coverage", - 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", - GIT_SSL_NO_VERIFY: "1", - GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem", - GIT_SSL_CAPATH: "/tmp/evil-git-ca-dir", - 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", - JAVA_OPTS: "-javaagent:/tmp/evil.jar", - GOFLAGS: "-mod=mod", - RUSTFLAGS: "-C link-args=-l/tmp/evil.so", - 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", - }, - }); + // Regression fixture intentionally feeds blocked override pivots into the sanitizer. + const baseEnv = { + PATH: "/usr/bin:/bin", + HOME: "/tmp/trusted-home", + ZDOTDIR: "/tmp/trusted-zdotdir", + CARGO_REGISTRIES_CRATES_IO_INDEX: "https://trusted.example/crates.io-index", + YARN_RC_FILENAME: ".trusted-yarnrc.yml", + }; + const overrides = { + PATH: "/tmp/evil", + HOME: "/tmp/evil-home", + ZDOTDIR: "/tmp/evil-zdotdir", + BASH_ENV: "/tmp/pwn.sh", + BROWSER: "/tmp/browser", + 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_DIR: "/tmp/evil-git-dir", + GIT_WORK_TREE: "/tmp/evil-work-tree", + GIT_COMMON_DIR: "/tmp/evil-common-dir", + GIT_EXEC_PATH: "/tmp/git-exec-path", + GIT_INDEX_FILE: "/tmp/evil-git-index", + GIT_OBJECT_DIRECTORY: "/tmp/evil-git-objects", + GIT_ALTERNATE_OBJECT_DIRECTORIES: "/tmp/evil-git-alt-objects", + GIT_NAMESPACE: "evil-namespace", + GIT_SEQUENCE_EDITOR: "/tmp/git-sequence-editor", + EDITOR: "/tmp/editor", + NPM_CONFIG_USERCONFIG: "/tmp/npmrc", + GIT_CONFIG_GLOBAL: "/tmp/gitconfig", + 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", + 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_PYTHON: "/tmp/evil-uv-python", + 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", + HELM_HOME: "/tmp/override-helm", + BASHOPTS: "xtrace", + FPATH: "/tmp/evil-fpath", + KSH_ENV: "/tmp/evil-ksh-env", + TCLLIBPATH: "/tmp/evil-tcllibpath", + NODE_REDIRECT_WARNINGS: "/tmp/node-warnings.log", + NODE_REPL_EXTERNAL_MODULE: "/tmp/pwn.js", + NODE_REPL_HISTORY: "/tmp/node-repl-history", + NODE_V8_COVERAGE: "/tmp/coverage", + 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", + GIT_SSL_NO_VERIFY: "1", + GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem", + GIT_SSL_CAPATH: "/tmp/evil-git-ca-dir", + 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", + JAVA_OPTS: "-javaagent:/tmp/evil.jar", + GOFLAGS: "-mod=mod", + RUSTFLAGS: "-C link-args=-l/tmp/evil.so", + 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", + }; + const env = sanitizeHostExecEnv({ baseEnv, overrides }); expect(env.PATH).toBe("/usr/bin:/bin"); expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); @@ -580,6 +596,10 @@ describe("sanitizeHostExecEnv", () => { EXINIT: "silent !touch /tmp/pwned", LUA_INIT_5_4: "os.execute('touch /tmp/pwned')", HOSTALIASES: "/tmp/evil-hostaliases", + BASHOPTS: "xtrace", + FPATH: "/tmp/evil-fpath", + KSH_ENV: "/tmp/evil-ksh-env", + TCLLIBPATH: "/tmp/evil-tcllibpath", AWS_CONTAINER_CREDENTIALS_FULL_URI: "http://169.254.170.2/credentials", AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/v2/credentials/abcd", CONFIG_SITE: "/tmp/evil-config-site", @@ -598,6 +618,10 @@ describe("sanitizeHostExecEnv", () => { expect(env.EXINIT).toBeUndefined(); expect(env.LUA_INIT_5_4).toBeUndefined(); expect(env.HOSTALIASES).toBeUndefined(); + expect(env.BASHOPTS).toBeUndefined(); + expect(env.FPATH).toBeUndefined(); + expect(env.KSH_ENV).toBeUndefined(); + expect(env.TCLLIBPATH).toBeUndefined(); expect(env.HTTPS_PROXY).toBe("http://trusted-proxy.example.test:8443"); expect(env.KUBECONFIG).toBe("/tmp/trusted-kubeconfig"); expect(env.GOOGLE_APPLICATION_CREDENTIALS).toBe("/tmp/trusted-gcp.json"); @@ -633,6 +657,10 @@ describe("sanitizeHostExecEnv", () => { overrides: { VIMINIT: ":!touch /tmp/pwned", HOSTALIASES: "/tmp/evil-hostaliases", + BASHOPTS: "xtrace", + FPATH: "/tmp/evil-fpath", + KSH_ENV: "/tmp/evil-ksh-env", + TCLLIBPATH: "/tmp/evil-tcllibpath", AWS_CONTAINER_CREDENTIALS_FULL_URI: "http://attacker/credentials", AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/attacker-credentials", ANSIBLE_CONFIG: "/tmp/override-ansible.cfg", @@ -662,6 +690,10 @@ describe("sanitizeHostExecEnv", () => { expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); expect(env.VIMINIT).toBeUndefined(); expect(env.HOSTALIASES).toBeUndefined(); + expect(env.BASHOPTS).toBeUndefined(); + expect(env.FPATH).toBeUndefined(); + expect(env.KSH_ENV).toBeUndefined(); + expect(env.TCLLIBPATH).toBeUndefined(); expect(env.AWS_CONTAINER_CREDENTIALS_FULL_URI).toBeUndefined(); expect(env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI).toBeUndefined(); expect(env.ANSIBLE_CONFIG).toBeUndefined(); @@ -943,85 +975,85 @@ describe("isDangerousHostEnvOverrideVarName", () => { describe("sanitizeHostExecEnvWithDiagnostics", () => { it("reports blocked and invalid requested overrides", () => { - const result = sanitizeHostExecEnvWithDiagnostics({ - baseEnv: { - PATH: "/usr/bin:/bin", - }, - 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", - 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", - 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_PYTHON: "/tmp/evil-uv-python", - 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", - GIT_DIR: "/tmp/evil-git-dir", - GIT_WORK_TREE: "/tmp/evil-work-tree", - GIT_COMMON_DIR: "/tmp/evil-common-dir", - GIT_INDEX_FILE: "/tmp/evil-git-index", - GIT_OBJECT_DIRECTORY: "/tmp/evil-git-objects", - GIT_ALTERNATE_OBJECT_DIRECTORIES: "/tmp/evil-git-alt-objects", - GIT_NAMESPACE: "evil-namespace", - 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", - CARGO_HOME: "/tmp/evil-cargo", - HGRCPATH: "/tmp/evil-hgrc", - MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", - MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", - HELM_HOME: "/tmp/evil-helm", - NODE_REDIRECT_WARNINGS: "/tmp/node-warnings.log", - NODE_REPL_EXTERNAL_MODULE: "/tmp/pwn.js", - NODE_REPL_HISTORY: "/tmp/node-repl-history", - NODE_V8_COVERAGE: "/tmp/coverage", - PYTHONUSERBASE: "/tmp/evil-python-userbase", - RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", - RUSTFLAGS: "-C link-args=-l/tmp/evil.so", - VIRTUAL_ENV: "/tmp/evil-venv", - JAVA_OPTS: "-javaagent:/tmp/evil.jar", - YARN_RC_FILENAME: ".evil-yarnrc.yml", - HTTPS_PROXY: "http://proxy.example.test:8080", - GIT_SSL_NO_VERIFY: "1", - GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem", - GIT_SSL_CAPATH: "/tmp/evil-git-capath", - NODE_TLS_REJECT_UNAUTHORIZED: "0", - SAFE_KEY: "ok", - "BAD-KEY": "bad", - }, - }); + // Diagnostics coverage needs the denied keys present at the sanitizer boundary. + const baseEnv = { + PATH: "/usr/bin:/bin", + }; + const 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", + 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", + 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_PYTHON: "/tmp/evil-uv-python", + 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", + GIT_DIR: "/tmp/evil-git-dir", + GIT_WORK_TREE: "/tmp/evil-work-tree", + GIT_COMMON_DIR: "/tmp/evil-common-dir", + GIT_INDEX_FILE: "/tmp/evil-git-index", + GIT_OBJECT_DIRECTORY: "/tmp/evil-git-objects", + GIT_ALTERNATE_OBJECT_DIRECTORIES: "/tmp/evil-git-alt-objects", + GIT_NAMESPACE: "evil-namespace", + 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", + CARGO_HOME: "/tmp/evil-cargo", + HGRCPATH: "/tmp/evil-hgrc", + MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)", + MFLAGS: "--eval=$(shell touch /tmp/pwned-too)", + HELM_HOME: "/tmp/evil-helm", + NODE_REDIRECT_WARNINGS: "/tmp/node-warnings.log", + NODE_REPL_EXTERNAL_MODULE: "/tmp/pwn.js", + NODE_REPL_HISTORY: "/tmp/node-repl-history", + NODE_V8_COVERAGE: "/tmp/coverage", + PYTHONUSERBASE: "/tmp/evil-python-userbase", + RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper", + RUSTFLAGS: "-C link-args=-l/tmp/evil.so", + VIRTUAL_ENV: "/tmp/evil-venv", + JAVA_OPTS: "-javaagent:/tmp/evil.jar", + YARN_RC_FILENAME: ".evil-yarnrc.yml", + HTTPS_PROXY: "http://proxy.example.test:8080", + GIT_SSL_NO_VERIFY: "1", + GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem", + GIT_SSL_CAPATH: "/tmp/evil-git-capath", + NODE_TLS_REJECT_UNAUTHORIZED: "0", + SAFE_KEY: "ok", + "BAD-KEY": "bad", + }; + const result = sanitizeHostExecEnvWithDiagnostics({ baseEnv, overrides }); expect(result.rejectedOverrideBlockedKeys).toEqual([ "AWS_SHARED_CREDENTIALS_FILE", @@ -1178,6 +1210,10 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { VIMINIT: ":!touch /tmp/pwned", LUA_INIT_5_4: "os.execute('touch /tmp/pwned')", HOSTALIASES: "/tmp/evil-hostaliases", + BASHOPTS: "xtrace", + FPATH: "/tmp/evil-fpath", + KSH_ENV: "/tmp/evil-ksh-env", + TCLLIBPATH: "/tmp/evil-tcllibpath", ANSIBLE_CONFIG: "/tmp/evil-ansible.cfg", ANSIBLE_REMOTE_TEMP: "/tmp/evil-ansible-remote", R_LIBS_USER: "/tmp/evil-r-libs-user", @@ -1205,12 +1241,16 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { "ANSIBLE_REMOTE_TEMP", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "BASHOPTS", "DATABASE_URL", + "FPATH", "GITHUB_TOKEN", "HOSTALIASES", + "KSH_ENV", "LUA_INIT_5_4", "R_LIBS_USER", "R_PROFILE_USER", + "TCLLIBPATH", "TF_CLI_CONFIG_FILE", "TF_PLUGIN_CACHE_DIR", "TF_VAR_ADMIN_CIDR", @@ -1228,6 +1268,10 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.VIMINIT).toBeUndefined(); expect(result.env.LUA_INIT_5_4).toBeUndefined(); expect(result.env.HOSTALIASES).toBeUndefined(); + expect(result.env.BASHOPTS).toBeUndefined(); + expect(result.env.FPATH).toBeUndefined(); + expect(result.env.KSH_ENV).toBeUndefined(); + expect(result.env.TCLLIBPATH).toBeUndefined(); expect(result.env.ANSIBLE_CONFIG).toBeUndefined(); expect(result.env.ANSIBLE_REMOTE_TEMP).toBeUndefined(); expect(result.env.R_LIBS_USER).toBeUndefined(); @@ -1590,12 +1634,9 @@ describe("git env exploit regression", () => { expect(fs.existsSync(marker)).toBe(true); clearMarker(marker); - const safeEnv = sanitizeHostExecEnv({ - baseEnv, - overrides: { - GIT_SSH_COMMAND: exploitValue, - }, - }); + // Exploit regression proves the sanitizer removes the Git command pivot before spawn. + const safeOverrides = { GIT_SSH_COMMAND: exploitValue }; + const safeEnv = sanitizeHostExecEnv({ baseEnv, overrides: safeOverrides }); await runGitLsRemote(gitPath, target, safeEnv); @@ -1643,12 +1684,9 @@ describe("compiler override exploit regression", () => { expect(fs.existsSync(marker)).toBe(true); clearMarker(marker); - const safeEnv = sanitizeHostExecEnv({ - baseEnv, - overrides: { - CC: exploitPath, - }, - }); + // Exploit regression proves compiler override pivots are blocked before make runs. + const safeOverrides = { CC: exploitPath }; + const safeEnv = sanitizeHostExecEnv({ baseEnv, overrides: safeOverrides }); await runMakeCommand(makePath, tempDir, safeEnv); @@ -1692,12 +1730,9 @@ describe("make env exploit regression", () => { const baselineTriggered = fs.existsSync(marker); clearMarker(marker); - const safeEnv = sanitizeHostExecEnv({ - baseEnv, - overrides: { - MAKEFLAGS: exploitValue, - }, - }); + // Exploit regression proves make flag pivots are blocked before child execution. + const safeOverrides = { MAKEFLAGS: exploitValue }; + const safeEnv = sanitizeHostExecEnv({ baseEnv, overrides: safeOverrides }); expect(safeEnv.MAKEFLAGS).toBeUndefined(); await runMakeCommand(makePath, tempDir, safeEnv);