fix(env): align inherited host exec env filtering (#59119)

* fix(env): block inherited host exec config vars

* fix(env): preserve trusted inherited proxy env

* fix(env): preserve inherited host exec vars

* fix(env): refresh host env policy parity artifacts

* test(env): align blocked override ordering

* docs(changelog): add host env policy parity entry

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
Agustin Rivera
2026-04-07 14:50:32 -07:00
committed by GitHub
parent 423a14e2be
commit fa82193c72
9 changed files with 299 additions and 177 deletions
+1
View File
@@ -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
@@ -6,182 +6,180 @@ import Foundation
enum HostEnvSecurityPolicy {
static let blockedKeys: Set<String> = [
"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<String> = [
"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_"
]
}
@@ -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");
+14
View File
@@ -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>,
): HostEnvSecurityPolicy;
export declare const HOST_ENV_SECURITY_POLICY: HostEnvSecurityPolicy;
+35
View File
@@ -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();
+10 -6
View File
@@ -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_"]
@@ -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]));
});
});
+70 -1
View File
@@ -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();
+13 -22
View File
@@ -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",