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
This commit is contained in:
Pavan Kumar Gondhi 2026-06-09 19:44:54 +05:30 committed by GitHub
parent 98d5c46530
commit 9f413acc18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 252 additions and 201 deletions

View file

@ -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<String> = [
"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"

View file

@ -673,7 +673,7 @@ Launches a local child process and communicates over stdin/stdout.
<Warning>
**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`.
</Warning>

View file

@ -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 <passive|active|timeSensitive>` and `--delivery <system|overlay|auto>`.
- 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`).

View file

@ -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.

View file

@ -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": [

View file

@ -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
}

View file

@ -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,
);

View file

@ -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);