fix(infra): expand host env security policy denylist [AI] (#63277)

* fix: address issue

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: close host env inherited sanitization gap

* fix: enforce host env reported baseline coverage

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* docs: add changelog entry for PR merge
This commit is contained in:
Pavan Kumar Gondhi
2026-04-10 11:36:39 +05:30
committed by GitHub
parent 71617ef2f0
commit 2d126fc623
13 changed files with 1165 additions and 21 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- fix(infra): expand host env security policy denylist [AI]. (#63277) Thanks @pgondhi987.
- fix(agents): guard nodes tool outPath against workspace boundary [AI-assisted]. (#63551) Thanks @pgondhi987.
- fix(qqbot): enforce media storage boundary for all outbound local file paths [AI]. (#63271) Thanks @pgondhi987.
- iMessage/self-chat: distinguish normal DM outbound rows from true self-chat using `destination_caller_id` plus chat participants, while preserving multi-handle self-chat aliases so outbound DM replies stop looping back as inbound messages. (#61619) Thanks @neeravmakwana.

View File

@@ -8,6 +8,8 @@ struct HostEnvOverrideDiagnostics: Equatable {
enum HostEnvSanitizer {
/// Generated from src/infra/host-env-security-policy.json via scripts/generate-host-env-security-policy-swift.mjs.
/// Parity is validated by src/infra/host-env-security.policy-parity.test.ts.
private static let blockedInheritedKeys = HostEnvSecurityPolicy.blockedInheritedKeys
private static let blockedInheritedPrefixes = HostEnvSecurityPolicy.blockedInheritedPrefixes
private static let blockedKeys = HostEnvSecurityPolicy.blockedKeys
private static let blockedPrefixes = HostEnvSecurityPolicy.blockedPrefixes
private static let blockedOverrideKeys = HostEnvSecurityPolicy.blockedOverrideKeys
@@ -28,6 +30,11 @@ enum HostEnvSanitizer {
return self.blockedPrefixes.contains(where: { upperKey.hasPrefix($0) })
}
private static func isBlockedInherited(_ upperKey: String) -> Bool {
if self.blockedInheritedKeys.contains(upperKey) { return true }
return self.blockedInheritedPrefixes.contains(where: { upperKey.hasPrefix($0) })
}
private static func isBlockedOverride(_ upperKey: String) -> Bool {
if self.blockedOverrideKeys.contains(upperKey) { return true }
return self.blockedOverridePrefixes.contains(where: { upperKey.hasPrefix($0) })
@@ -113,7 +120,7 @@ enum HostEnvSanitizer {
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { continue }
let upper = key.uppercased()
if self.isBlocked(upper) { continue }
if self.isBlockedInherited(upper) { continue }
merged[key] = value
}

View File

@@ -5,20 +5,232 @@
import Foundation
enum HostEnvSecurityPolicy {
static let blockedInheritedKeys: Set<String> = [
"_JAVA_OPTIONS",
"AMQP_URL",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
"ANSIBLE_CONFIG",
"ANSIBLE_CONNECTION_PLUGINS",
"ANSIBLE_FILTER_PLUGINS",
"ANSIBLE_INVENTORY_PLUGINS",
"ANSIBLE_LIBRARY",
"ANSIBLE_LOOKUP_PLUGINS",
"ANSIBLE_MODULE_UTILS",
"ANSIBLE_REMOTE_TEMP",
"ANSIBLE_ROLES_PATH",
"ANSIBLE_STRATEGY_PLUGINS",
"ANT_OPTS",
"AWS_ACCESS_KEY_ID",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"BASH_ENV",
"BROWSER",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"BZR_EDITOR",
"BZR_PLUGIN_PATH",
"BZR_SSH",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CATALINA_OPTS",
"CC",
"CFLAGS",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"COMPOSER_HOME",
"CONFIG_SHELL",
"CONFIG_SITE",
"CORECLR_PROFILER",
"CORECLR_PROFILER_PATH",
"CPATH",
"CPLUS_INCLUDE_PATH",
"CURL_HOME",
"CXX",
"DATABASE_URL",
"DENO_DIR",
"DOTNET_ADDITIONAL_DEPS",
"DOTNET_STARTUP_HOOKS",
"EDITOR",
"ELIXIR_ERL_OPTIONS",
"EMACSLOADPATH",
"ENV",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"EXINIT",
"FCEDIT",
"GCONV_PATH",
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
"GIT_DIR",
"GIT_EDITOR",
"GIT_EXEC_PATH",
"GIT_EXTERNAL_DIFF",
"GIT_HOOK_PATH",
"GIT_INDEX_FILE",
"GIT_NAMESPACE",
"GIT_OBJECT_DIRECTORY",
"GIT_PROXY_COMMAND",
"GIT_SEQUENCE_EDITOR",
"GIT_SSH",
"GIT_SSH_COMMAND",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_TEMPLATE_DIR",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GLIBC_TUNABLES",
"GOENV",
"GOFLAGS",
"GONOPROXY",
"GONOSUMCHECK",
"GONOSUMDB",
"GOPATH",
"GOPRIVATE",
"GOPROXY",
"GRADLE_OPTS",
"GVIMINIT",
"HELM_HOME",
"HELM_PLUGINS",
"HGRCPATH",
"HOSTALIASES",
"IFS",
"JAVA_OPTS",
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
"JULIA_EDITOR",
"LDFLAGS",
"LESSCLOSE",
"LESSOPEN",
"LIBRARY_PATH",
"LUA_CPATH",
"LUA_INIT",
"LUA_INIT_5_1",
"LUA_INIT_5_2",
"LUA_INIT_5_3",
"LUA_INIT_5_4",
"LUA_PATH",
"MAKEFLAGS",
"MAVEN_OPTS",
"MFLAGS",
"MONGODB_URI",
"MYVIMRC",
"NODE_AUTH_TOKEN",
"NODE_OPTIONS",
"NODE_PATH",
"NPM_TOKEN",
"OBJC_INCLUDE_PATH",
"OPENSSL_CONF",
"OPENSSL_ENGINES",
"PACKER_PLUGIN_PATH",
"PERL5DB",
"PERL5DBCMD",
"PERL5LIB",
"PERL5OPT",
"PHP_INI_SCAN_DIR",
"PHPRC",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
"PIP_INDEX_URL",
"PIP_PYPI_URL",
"PIP_TRUSTED_HOST",
"PROMPT_COMMAND",
"PS4",
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_LIBS_USER",
"R_PROFILE",
"R_PROFILE_USER",
"REDIS_URL",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
"SSH_ASKPASS",
"SSLKEYLOGFILE",
"SUDO_ASKPASS",
"SUDO_EDITOR",
"SVN_EDITOR",
"SVN_SSH",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"UV_DEFAULT_INDEX",
"UV_EXTRA_INDEX_URL",
"UV_INDEX",
"UV_INDEX_URL",
"UV_PYTHON",
"VAGRANT_VAGRANTFILE",
"VIMINIT",
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"XDG_CONFIG_DIRS",
"XDG_CONFIG_HOME",
"YARN_RC_FILENAME"
]
static let blockedInheritedPrefixes: [String] = [
"BASH_FUNC_",
"DYLD_",
"LD_"
]
static let blockedKeys: Set<String> = [
"_JAVA_OPTIONS",
"ANT_OPTS",
"BASH_ENV",
"BROWSER",
"BZR_EDITOR",
"BZR_PLUGIN_PATH",
"BZR_SSH",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CATALINA_OPTS",
"CC",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"CONFIG_SHELL",
"CONFIG_SITE",
"CORECLR_PROFILER",
"CXX",
"DOTNET_ADDITIONAL_DEPS",
"DOTNET_STARTUP_HOOKS",
"ELIXIR_ERL_OPTIONS",
"EMACSLOADPATH",
"ENV",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"EXINIT",
"GCONV_PATH",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_COMMON_DIR",
@@ -26,6 +238,7 @@ enum HostEnvSecurityPolicy {
"GIT_EDITOR",
"GIT_EXEC_PATH",
"GIT_EXTERNAL_DIFF",
"GIT_HOOK_PATH",
"GIT_INDEX_FILE",
"GIT_NAMESPACE",
"GIT_OBJECT_DIRECTORY",
@@ -37,42 +250,85 @@ enum HostEnvSecurityPolicy {
"GIT_WORK_TREE",
"GLIBC_TUNABLES",
"GRADLE_OPTS",
"GVIMINIT",
"HELM_PLUGINS",
"HGRCPATH",
"HOSTALIASES",
"IFS",
"JAVA_OPTS",
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
"JULIA_EDITOR",
"LUA_INIT",
"LUA_INIT_5_1",
"LUA_INIT_5_2",
"LUA_INIT_5_3",
"LUA_INIT_5_4",
"MAKEFLAGS",
"MAVEN_OPTS",
"MFLAGS",
"MYVIMRC",
"NODE_OPTIONS",
"NODE_PATH",
"PACKER_PLUGIN_PATH",
"PERL5LIB",
"PERL5OPT",
"PS4",
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_PROFILE",
"R_PROFILE_USER",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
"SSLKEYLOGFILE"
"SSLKEYLOGFILE",
"SUDO_ASKPASS",
"SVN_EDITOR",
"SVN_SSH",
"VAGRANT_VAGRANTFILE",
"VIMINIT"
]
static let blockedOverrideKeys: Set<String> = [
"ALL_PROXY",
"AMQP_URL",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
"ANSIBLE_CONFIG",
"ANSIBLE_CONNECTION_PLUGINS",
"ANSIBLE_FILTER_PLUGINS",
"ANSIBLE_INVENTORY_PLUGINS",
"ANSIBLE_LIBRARY",
"ANSIBLE_LOOKUP_PLUGINS",
"ANSIBLE_MODULE_UTILS",
"ANSIBLE_REMOTE_TEMP",
"ANSIBLE_ROLES_PATH",
"ANSIBLE_STRATEGY_PLUGINS",
"AWS_ACCESS_KEY_ID",
"AWS_CONFIG_FILE",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"AZURE_AUTH_LOCATION",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CFLAGS",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
@@ -82,6 +338,7 @@ enum HostEnvSecurityPolicy {
"CPLUS_INCLUDE_PATH",
"CURL_CA_BUNDLE",
"CURL_HOME",
"DATABASE_URL",
"DENO_DIR",
"DOCKER_CERT_PATH",
"DOCKER_CONTEXT",
@@ -91,6 +348,7 @@ enum HostEnvSecurityPolicy {
"FCEDIT",
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
@@ -106,6 +364,8 @@ enum HostEnvSecurityPolicy {
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GOENV",
"GOFLAGS",
"GONOPROXY",
@@ -123,6 +383,7 @@ enum HostEnvSecurityPolicy {
"HTTP_PROXY",
"HTTPS_PROXY",
"KUBECONFIG",
"LDFLAGS",
"LESSCLOSE",
"LESSOPEN",
"LIBRARY_PATH",
@@ -131,9 +392,12 @@ enum HostEnvSecurityPolicy {
"MAKEFLAGS",
"MANPAGER",
"MFLAGS",
"MONGODB_URI",
"NO_PROXY",
"NODE_AUTH_TOKEN",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NPM_TOKEN",
"OBJC_INCLUDE_PATH",
"OPENSSL_CONF",
"OPENSSL_ENGINES",
@@ -151,13 +415,18 @@ enum HostEnvSecurityPolicy {
"PROMPT_COMMAND",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_LIBS_USER",
"REDIS_URL",
"REQUESTS_CA_BUNDLE",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"SSH_ASKPASS",
"SSH_AUTH_SOCK",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"SUDO_EDITOR",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"UV_DEFAULT_INDEX",
"UV_EXTRA_INDEX_URL",
"UV_INDEX",
@@ -166,6 +435,7 @@ enum HostEnvSecurityPolicy {
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"XDG_CONFIG_DIRS",
"XDG_CONFIG_HOME",
"YARN_RC_FILENAME",
"ZDOTDIR"
@@ -174,7 +444,8 @@ enum HostEnvSecurityPolicy {
static let blockedOverridePrefixes: [String] = [
"CARGO_REGISTRIES_",
"GIT_CONFIG_",
"NPM_CONFIG_"
"NPM_CONFIG_",
"TF_VAR_"
]
static let blockedPrefixes: [String] = [

View File

@@ -37,6 +37,14 @@ const generated = `// Generated file. Do not edit directly.
import Foundation
enum HostEnvSecurityPolicy {
static let blockedInheritedKeys: Set<String> = [
${renderSwiftStringArray(policy.blockedInheritedKeys)}
]
static let blockedInheritedPrefixes: [String] = [
${renderSwiftStringArray(policy.blockedInheritedPrefixes)}
]
static let blockedKeys: Set<String> = [
${renderSwiftStringArray(policy.blockedKeys)}
]

View File

@@ -9,7 +9,7 @@ import {
type ExecTarget,
} from "../infra/exec-approvals.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { isDangerousHostEnvVarName } from "../infra/host-env-security.js";
import { isDangerousHostInheritedEnvVarName } from "../infra/host-env-security.js";
import { findPathKey, mergePathPrepend } from "../infra/path-prepend.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { scopedHeartbeatWakeOptions } from "../routing/session-key.js";
@@ -72,7 +72,7 @@ export function sanitizeHostBaseEnv(env: Record<string, string>): Record<string,
sanitized[key] = value;
continue;
}
if (isDangerousHostEnvVarName(upperKey)) {
if (isDangerousHostInheritedEnvVarName(upperKey)) {
continue;
}
sanitized[key] = value;
@@ -86,7 +86,7 @@ export function validateHostEnv(env: Record<string, string>): void {
const upperKey = key.toUpperCase();
// 1. Block known dangerous variables (Fail Closed)
if (isDangerousHostEnvVarName(upperKey)) {
if (isDangerousHostInheritedEnvVarName(upperKey)) {
throw new Error(
`Security Violation: Environment variable '${key}' is forbidden during host execution.`,
);

View File

@@ -1,6 +1,9 @@
export type HostEnvSecurityPolicy = Readonly<{
blockedEverywhereKeys: readonly string[];
blockedOverrideOnlyKeys: readonly string[];
allowedInheritedOverrideOnlyKeys: readonly string[];
blockedInheritedKeys: readonly string[];
blockedInheritedPrefixes: readonly string[];
blockedPrefixes: readonly string[];
blockedOverridePrefixes: readonly string[];
blockedKeys: readonly string[];

View File

@@ -11,12 +11,26 @@ function sortUniqueUppercase(values) {
function derivePolicyArrays(policy) {
const blockedEverywhereKeys = policy.blockedEverywhereKeys ?? [];
const blockedOverrideOnlyKeys = policy.blockedOverrideOnlyKeys ?? [];
const allowedInheritedOverrideOnlyKeys = policy.allowedInheritedOverrideOnlyKeys ?? [];
const allowedInheritedOverrideOnlyUpper = new Set(
allowedInheritedOverrideOnlyKeys.map((value) => value.toUpperCase()),
);
const blockedPrefixes = policy.blockedPrefixes ?? [];
const blockedOverridePrefixes = policy.blockedOverridePrefixes ?? [];
const blockedInheritedPrefixes = policy.blockedInheritedPrefixes ?? blockedPrefixes;
return {
blockedInheritedKeys: sortUniqueUppercase([
...blockedEverywhereKeys,
...blockedOverrideOnlyKeys.filter(
(value) => !allowedInheritedOverrideOnlyUpper.has(value.toUpperCase()),
),
]),
blockedInheritedPrefixes: sortUniqueUppercase(blockedInheritedPrefixes),
blockedKeys: sortUniqueUppercase(blockedEverywhereKeys),
blockedOverrideKeys: sortUniqueUppercase(blockedOverrideOnlyKeys),
blockedPrefixes: sortUniqueUppercase(policy.blockedPrefixes ?? []),
blockedOverridePrefixes: sortUniqueUppercase(policy.blockedOverridePrefixes ?? []),
blockedPrefixes: sortUniqueUppercase(blockedPrefixes),
blockedOverridePrefixes: sortUniqueUppercase(blockedOverridePrefixes),
};
}
@@ -25,6 +39,11 @@ export function loadHostEnvSecurityPolicy(rawPolicy = HOST_ENV_SECURITY_POLICY_J
return Object.freeze({
blockedEverywhereKeys: Object.freeze(rawPolicy.blockedEverywhereKeys ?? []),
blockedOverrideOnlyKeys: Object.freeze(rawPolicy.blockedOverrideOnlyKeys ?? []),
allowedInheritedOverrideOnlyKeys: Object.freeze(
rawPolicy.allowedInheritedOverrideOnlyKeys ?? [],
),
blockedInheritedKeys: derived.blockedInheritedKeys,
blockedInheritedPrefixes: derived.blockedInheritedPrefixes,
blockedPrefixes: derived.blockedPrefixes,
blockedOverridePrefixes: derived.blockedOverridePrefixes,
blockedKeys: derived.blockedKeys,

View File

@@ -53,7 +53,43 @@
"SBT_OPTS",
"GRADLE_OPTS",
"ANT_OPTS",
"HGRCPATH"
"HGRCPATH",
"EXINIT",
"VIMINIT",
"MYVIMRC",
"GVIMINIT",
"LUA_INIT",
"LUA_INIT_5_1",
"LUA_INIT_5_2",
"LUA_INIT_5_3",
"LUA_INIT_5_4",
"EMACSLOADPATH",
"RUBYSHELL",
"GIT_HOOK_PATH",
"SVN_EDITOR",
"SVN_SSH",
"BZR_EDITOR",
"BZR_SSH",
"BZR_PLUGIN_PATH",
"SUDO_ASKPASS",
"JULIA_EDITOR",
"CONFIG_SITE",
"CONFIG_SHELL",
"CMAKE_TOOLCHAIN_FILE",
"CATALINA_OPTS",
"CORECLR_PROFILER",
"HELM_PLUGINS",
"PACKER_PLUGIN_PATH",
"VAGRANT_VAGRANTFILE",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"ELIXIR_ERL_OPTIONS",
"R_ENVIRON",
"R_PROFILE",
"R_ENVIRON_USER",
"R_PROFILE_USER",
"HOSTALIASES"
],
"blockedOverrideOnlyKeys": [
"HOME",
@@ -93,6 +129,7 @@
"WGETRC",
"CURL_HOME",
"CLASSPATH",
"CFLAGS",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"GOFLAGS",
@@ -130,15 +167,11 @@
"UV_DEFAULT_INDEX",
"DOCKER_CONTEXT",
"LIBRARY_PATH",
"LDFLAGS",
"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",
@@ -160,15 +193,78 @@
"COMPOSER_HOME",
"CARGO_BUILD_RUSTC_WRAPPER",
"XDG_CONFIG_HOME",
"XDG_CONFIG_DIRS",
"AWS_CONFIG_FILE",
"KUBECONFIG",
"GOOGLE_APPLICATION_CREDENTIALS",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"AZURE_AUTH_LOCATION",
"CARGO_HOME",
"HELM_HOME"
"HELM_HOME",
"ANSIBLE_CONFIG",
"ANSIBLE_LIBRARY",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
"ANSIBLE_CONNECTION_PLUGINS",
"ANSIBLE_FILTER_PLUGINS",
"ANSIBLE_INVENTORY_PLUGINS",
"ANSIBLE_LOOKUP_PLUGINS",
"ANSIBLE_MODULE_UTILS",
"ANSIBLE_REMOTE_TEMP",
"ANSIBLE_ROLES_PATH",
"ANSIBLE_STRATEGY_PLUGINS",
"R_LIBS_USER",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"AMQP_URL",
"AWS_ACCESS_KEY_ID",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"DATABASE_URL",
"GH_TOKEN",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"MONGODB_URI",
"NODE_AUTH_TOKEN",
"NPM_TOKEN",
"REDIS_URL",
"SSH_AUTH_SOCK"
],
"blockedOverridePrefixes": ["GIT_CONFIG_", "NPM_CONFIG_", "CARGO_REGISTRIES_"],
"allowedInheritedOverrideOnlyKeys": [
"ALL_PROXY",
"AWS_CONFIG_FILE",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"AZURE_AUTH_LOCATION",
"CURL_CA_BUNDLE",
"DOCKER_CERT_PATH",
"DOCKER_CONTEXT",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"GIT_PAGER",
"GOOGLE_APPLICATION_CREDENTIALS",
"GRADLE_USER_HOME",
"HISTFILE",
"HOME",
"HTTPS_PROXY",
"HTTP_PROXY",
"KUBECONFIG",
"MANPAGER",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NO_PROXY",
"PAGER",
"REQUESTS_CA_BUNDLE",
"SSH_AUTH_SOCK",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"ZDOTDIR"
],
"blockedOverridePrefixes": ["GIT_CONFIG_", "NPM_CONFIG_", "CARGO_REGISTRIES_", "TF_VAR_"],
"blockedPrefixes": ["DYLD_", "LD_", "BASH_FUNC_"]
}

View File

@@ -36,6 +36,14 @@ describe("host env security policy parity", () => {
const sanitizerSource = fs.readFileSync(sanitizerSwiftPath, "utf8");
const swiftBlockedKeys = parseSwiftStringArray(generatedSource, "static let blockedKeys");
const swiftBlockedInheritedKeys = parseSwiftStringArray(
generatedSource,
"static let blockedInheritedKeys",
);
const swiftBlockedInheritedPrefixes = parseSwiftStringArray(
generatedSource,
"static let blockedInheritedPrefixes",
);
const swiftBlockedOverrideKeys = parseSwiftStringArray(
generatedSource,
"static let blockedOverrideKeys",
@@ -49,11 +57,19 @@ describe("host env security policy parity", () => {
"static let blockedPrefixes",
);
expect(swiftBlockedInheritedKeys).toEqual(policy.blockedInheritedKeys);
expect(swiftBlockedInheritedPrefixes).toEqual(policy.blockedInheritedPrefixes ?? []);
expect(swiftBlockedKeys).toEqual(policy.blockedKeys);
expect(swiftBlockedOverrideKeys).toEqual(policy.blockedOverrideKeys ?? []);
expect(swiftBlockedOverridePrefixes).toEqual(policy.blockedOverridePrefixes ?? []);
expect(swiftBlockedPrefixes).toEqual(policy.blockedPrefixes);
expect(sanitizerSource).toContain(
"private static let blockedInheritedKeys = HostEnvSecurityPolicy.blockedInheritedKeys",
);
expect(sanitizerSource).toContain(
"private static let blockedInheritedPrefixes = HostEnvSecurityPolicy.blockedInheritedPrefixes",
);
expect(sanitizerSource).toContain(
"private static let blockedKeys = HostEnvSecurityPolicy.blockedKeys",
);
@@ -73,8 +89,24 @@ describe("host env security policy parity", () => {
const policyPath = path.join(repoRoot, "src/infra/host-env-security-policy.json");
const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
const policy = loadHostEnvSecurityPolicy(rawPolicy);
const allowedInheritedOverrideOnlyKeys = new Set(
(rawPolicy.allowedInheritedOverrideOnlyKeys ?? []).map((value: string) =>
value.toUpperCase(),
),
);
expect(policy.blockedKeys).toEqual(sortUnique([...policy.blockedEverywhereKeys]));
expect(policy.blockedOverrideKeys).toEqual(sortUnique([...policy.blockedOverrideOnlyKeys]));
expect(policy.blockedInheritedKeys).toEqual(
sortUnique([
...policy.blockedEverywhereKeys,
...policy.blockedOverrideOnlyKeys.filter(
(value) => !allowedInheritedOverrideOnlyKeys.has(value.toUpperCase()),
),
]),
);
expect(policy.blockedInheritedPrefixes).toEqual(
sortUnique(rawPolicy.blockedInheritedPrefixes ?? rawPolicy.blockedPrefixes ?? []),
);
});
});

View File

@@ -0,0 +1,241 @@
{
"source": "OpenClaw host env dangerous-variable baseline (reported GHSA class)",
"generatedAt": "2026-04-10",
"reportedDangerousEverywhereKeys": [
"_JAVA_OPTIONS",
"ANT_OPTS",
"BASH_ENV",
"BROWSER",
"BZR_EDITOR",
"BZR_PLUGIN_PATH",
"BZR_SSH",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CATALINA_OPTS",
"CC",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"CONFIG_SHELL",
"CONFIG_SITE",
"CORECLR_PROFILER",
"CXX",
"DOTNET_ADDITIONAL_DEPS",
"DOTNET_STARTUP_HOOKS",
"ELIXIR_ERL_OPTIONS",
"EMACSLOADPATH",
"ENV",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"EXINIT",
"GCONV_PATH",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_COMMON_DIR",
"GIT_DIR",
"GIT_EDITOR",
"GIT_EXEC_PATH",
"GIT_EXTERNAL_DIFF",
"GIT_HOOK_PATH",
"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",
"GVIMINIT",
"HELM_PLUGINS",
"HGRCPATH",
"HOSTALIASES",
"IFS",
"JAVA_OPTS",
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
"JULIA_EDITOR",
"LUA_INIT",
"LUA_INIT_5_1",
"LUA_INIT_5_2",
"LUA_INIT_5_3",
"LUA_INIT_5_4",
"MAKEFLAGS",
"MAVEN_OPTS",
"MFLAGS",
"MYVIMRC",
"NODE_OPTIONS",
"NODE_PATH",
"PACKER_PLUGIN_PATH",
"PERL5LIB",
"PERL5OPT",
"PS4",
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_PROFILE",
"R_PROFILE_USER",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
"SSLKEYLOGFILE",
"SUDO_ASKPASS",
"SVN_EDITOR",
"SVN_SSH",
"VAGRANT_VAGRANTFILE",
"VIMINIT"
],
"reportedDangerousOverrideOnlyKeys": [
"ALL_PROXY",
"AMQP_URL",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
"ANSIBLE_CONFIG",
"ANSIBLE_CONNECTION_PLUGINS",
"ANSIBLE_FILTER_PLUGINS",
"ANSIBLE_INVENTORY_PLUGINS",
"ANSIBLE_LIBRARY",
"ANSIBLE_LOOKUP_PLUGINS",
"ANSIBLE_MODULE_UTILS",
"ANSIBLE_REMOTE_TEMP",
"ANSIBLE_ROLES_PATH",
"ANSIBLE_STRATEGY_PLUGINS",
"AWS_ACCESS_KEY_ID",
"AWS_CONFIG_FILE",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"AZURE_AUTH_LOCATION",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CFLAGS",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
"COMPOSER_HOME",
"CORECLR_PROFILER_PATH",
"CPATH",
"CPLUS_INCLUDE_PATH",
"CURL_CA_BUNDLE",
"CURL_HOME",
"DATABASE_URL",
"DENO_DIR",
"DOCKER_CERT_PATH",
"DOCKER_CONTEXT",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"EDITOR",
"FCEDIT",
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"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",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GOENV",
"GOFLAGS",
"GONOPROXY",
"GONOSUMCHECK",
"GONOSUMDB",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOPATH",
"GOPRIVATE",
"GOPROXY",
"GRADLE_USER_HOME",
"HELM_HOME",
"HGRCPATH",
"HISTFILE",
"HOME",
"HTTP_PROXY",
"HTTPS_PROXY",
"KUBECONFIG",
"LDFLAGS",
"LESSCLOSE",
"LESSOPEN",
"LIBRARY_PATH",
"LUA_CPATH",
"LUA_PATH",
"MAKEFLAGS",
"MANPAGER",
"MFLAGS",
"MONGODB_URI",
"NO_PROXY",
"NODE_AUTH_TOKEN",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NPM_TOKEN",
"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_TRUSTED_HOST",
"PROMPT_COMMAND",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_LIBS_USER",
"REDIS_URL",
"REQUESTS_CA_BUNDLE",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"SSH_ASKPASS",
"SSH_AUTH_SOCK",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"SUDO_EDITOR",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"UV_DEFAULT_INDEX",
"UV_EXTRA_INDEX_URL",
"UV_INDEX",
"UV_INDEX_URL",
"UV_PYTHON",
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"XDG_CONFIG_DIRS",
"XDG_CONFIG_HOME",
"YARN_RC_FILENAME",
"ZDOTDIR"
],
"expectedTotalReportedEntries": 232
}

View File

@@ -0,0 +1,180 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
isDangerousHostInheritedEnvVarName,
sanitizeHostExecEnv,
sanitizeHostExecEnvWithDiagnostics,
} from "./host-env-security.js";
type HostEnvReportedBaseline = {
source: string;
generatedAt: string;
reportedDangerousEverywhereKeys: string[];
reportedDangerousOverrideOnlyKeys: string[];
expectedTotalReportedEntries: number;
};
const INHERITED_ALLOWLIST_RATIONALE: Record<string, string> = {
ALL_PROXY: "Trusted inherited global proxy route from operator runtime.",
AWS_CONFIG_FILE: "Trusted inherited AWS CLI/SDK config path selected by operator.",
AWS_SHARED_CREDENTIALS_FILE:
"Trusted inherited AWS shared credentials path selected by operator.",
AWS_WEB_IDENTITY_TOKEN_FILE: "Trusted inherited AWS web identity token path.",
AZURE_AUTH_LOCATION: "Trusted inherited Azure auth location selected by operator.",
CURL_CA_BUNDLE: "Trusted inherited CA bundle path for TLS validation.",
DOCKER_CERT_PATH: "Trusted inherited Docker client certificate location.",
DOCKER_CONTEXT: "Trusted inherited Docker context selector from operator runtime.",
DOCKER_HOST: "Trusted inherited Docker endpoint selected by operator.",
DOCKER_TLS_VERIFY: "Trusted inherited Docker TLS verification mode.",
GIT_PAGER: "Trusted inherited interactive pager preference.",
GOOGLE_APPLICATION_CREDENTIALS:
"Trusted inherited Google application credentials path selected by operator.",
GRADLE_USER_HOME: "Trusted inherited tool cache directory location.",
HISTFILE: "Trusted inherited shell history path.",
HOME: "Trusted inherited process home-directory context.",
HTTPS_PROXY: "Trusted inherited HTTPS proxy route from operator runtime.",
HTTP_PROXY: "Trusted inherited HTTP proxy route from operator runtime.",
KUBECONFIG: "Trusted inherited Kubernetes config path selected by operator.",
MANPAGER: "Trusted inherited manual-page pager preference.",
NODE_EXTRA_CA_CERTS: "Trusted inherited extra Node CA trust roots.",
NODE_TLS_REJECT_UNAUTHORIZED: "Trusted inherited Node TLS mode from runtime policy.",
NO_PROXY: "Trusted inherited proxy bypass list from operator runtime.",
PAGER: "Trusted inherited default pager preference.",
REQUESTS_CA_BUNDLE: "Trusted inherited Python requests CA bundle path.",
SSH_AUTH_SOCK: "Trusted inherited SSH agent socket from operator runtime.",
SSL_CERT_DIR: "Trusted inherited OpenSSL certificate directory path.",
SSL_CERT_FILE: "Trusted inherited OpenSSL certificate file path.",
ZDOTDIR: "Trusted inherited shell startup directory boundary.",
};
function readBaselineAndPolicy(): {
baseline: HostEnvReportedBaseline;
allowedInheritedOverrideOnlyKeys: string[];
} {
const repoRoot = process.cwd();
const baselinePath = path.join(repoRoot, "src/infra/host-env-security.reported-baseline.json");
const policyPath = path.join(repoRoot, "src/infra/host-env-security-policy.json");
const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8")) as HostEnvReportedBaseline;
const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")) as {
allowedInheritedOverrideOnlyKeys?: string[];
};
return {
baseline,
allowedInheritedOverrideOnlyKeys: (policy.allowedInheritedOverrideOnlyKeys ?? []).map((key) =>
key.toUpperCase(),
),
};
}
function sortUniqueUpper(values: string[]): string[] {
return Array.from(new Set(values.map((value) => value.toUpperCase()))).toSorted((a, b) =>
a.localeCompare(b),
);
}
describe("host env reported baseline coverage", () => {
it("keeps the fixed reported dangerous env baseline fully covered by inherited + override sanitization", () => {
const { baseline, allowedInheritedOverrideOnlyKeys } = readBaselineAndPolicy();
expect(
baseline.reportedDangerousEverywhereKeys.length +
baseline.reportedDangerousOverrideOnlyKeys.length,
).toBe(baseline.expectedTotalReportedEntries);
expect(baseline.expectedTotalReportedEntries).toBe(232);
expect(sortUniqueUpper(baseline.reportedDangerousEverywhereKeys)).toEqual(
baseline.reportedDangerousEverywhereKeys,
);
expect(sortUniqueUpper(baseline.reportedDangerousOverrideOnlyKeys)).toEqual(
baseline.reportedDangerousOverrideOnlyKeys,
);
const inheritedInput: Record<string, string> = {
PATH: "/usr/bin:/bin",
};
for (const key of baseline.reportedDangerousEverywhereKeys) {
inheritedInput[key] = `${key.toLowerCase()}-from-inherited`;
}
for (const key of baseline.reportedDangerousOverrideOnlyKeys) {
inheritedInput[key] = `${key.toLowerCase()}-from-inherited`;
}
const inheritedSanitized = sanitizeHostExecEnv({ baseEnv: inheritedInput });
for (const key of baseline.reportedDangerousEverywhereKeys) {
expect(isDangerousHostEnvVarName(key)).toBe(true);
expect(isDangerousHostInheritedEnvVarName(key)).toBe(true);
expect(inheritedSanitized[key]).toBeUndefined();
}
const inheritedAllowlist = new Set(allowedInheritedOverrideOnlyKeys);
for (const key of baseline.reportedDangerousOverrideOnlyKeys) {
expect(isDangerousHostEnvOverrideVarName(key)).toBe(true);
if (inheritedAllowlist.has(key)) {
expect(isDangerousHostInheritedEnvVarName(key)).toBe(false);
expect(inheritedSanitized[key]).toBe(`${key.toLowerCase()}-from-inherited`);
} else {
expect(isDangerousHostInheritedEnvVarName(key)).toBe(true);
expect(inheritedSanitized[key]).toBeUndefined();
}
}
const overrideInput: Record<string, string> = {};
for (const key of baseline.reportedDangerousEverywhereKeys) {
overrideInput[key] = `${key.toLowerCase()}-from-override`;
}
for (const key of baseline.reportedDangerousOverrideOnlyKeys) {
overrideInput[key] = `${key.toLowerCase()}-from-override`;
}
const overrideResult = sanitizeHostExecEnvWithDiagnostics({
baseEnv: { PATH: "/usr/bin:/bin" },
overrides: overrideInput,
});
const expectedRejectedOverrideKeys = sortUniqueUpper([
...baseline.reportedDangerousEverywhereKeys,
...baseline.reportedDangerousOverrideOnlyKeys,
]);
expect(overrideResult.rejectedOverrideBlockedKeys).toEqual(expectedRejectedOverrideKeys);
expect(overrideResult.rejectedOverrideInvalidKeys).toEqual([]);
for (const key of expectedRejectedOverrideKeys) {
expect(overrideResult.env[key]).toBeUndefined();
}
});
it("documents and enforces rationale for every inherited allowlist exception", () => {
const { allowedInheritedOverrideOnlyKeys } = readBaselineAndPolicy();
const expectedAllowlistKeys = Object.keys(INHERITED_ALLOWLIST_RATIONALE).toSorted((a, b) =>
a.localeCompare(b),
);
expect(allowedInheritedOverrideOnlyKeys.toSorted((a, b) => a.localeCompare(b))).toEqual(
expectedAllowlistKeys,
);
for (const key of expectedAllowlistKeys) {
expect(INHERITED_ALLOWLIST_RATIONALE[key].trim().length).toBeGreaterThan(0);
expect(isDangerousHostInheritedEnvVarName(key)).toBe(false);
expect(isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)).toBe(true);
const inheritedSanitized = sanitizeHostExecEnv({
baseEnv: {
PATH: "/usr/bin:/bin",
[key]: `${key.toLowerCase()}-trusted-inherited`,
},
});
expect(inheritedSanitized[key]).toBe(`${key.toLowerCase()}-trusted-inherited`);
const overrideResult = sanitizeHostExecEnvWithDiagnostics({
baseEnv: { PATH: "/usr/bin:/bin" },
overrides: {
[key]: `${key.toLowerCase()}-untrusted-override`,
},
});
expect(overrideResult.rejectedOverrideBlockedKeys).toEqual([key]);
expect(overrideResult.rejectedOverrideInvalidKeys).toEqual([]);
expect(overrideResult.env[key]).toBeUndefined();
}
});
});

View File

@@ -5,6 +5,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostInheritedEnvVarName,
isDangerousHostEnvVarName,
normalizeEnvVarKey,
sanitizeHostExecEnv,
@@ -223,6 +224,68 @@ describe("isDangerousHostEnvVarName", () => {
expect(isDangerousHostEnvVarName("FOO")).toBe(false);
expect(isDangerousHostEnvVarName("GRADLE_USER_HOME")).toBe(false);
});
it("blocks newly added startup, orchestration, and resolver env keys", () => {
const keys = [
"VIMINIT",
"EXINIT",
"MYVIMRC",
"GVIMINIT",
"LUA_INIT",
"LUA_INIT_5_4",
"HOSTALIASES",
"CONFIG_SITE",
"CONFIG_SHELL",
"CMAKE_TOOLCHAIN_FILE",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"R_ENVIRON",
"R_PROFILE_USER",
] as const;
for (const key of keys) {
expect(isDangerousHostEnvVarName(key)).toBe(true);
expect(isDangerousHostEnvVarName(key.toLowerCase())).toBe(true);
}
expect(isDangerousHostEnvVarName("ANSIBLE_CONFIG")).toBe(false);
expect(isDangerousHostEnvVarName("ANSIBLE_LIBRARY")).toBe(false);
expect(isDangerousHostEnvVarName("TF_CLI_CONFIG_FILE")).toBe(false);
expect(isDangerousHostEnvVarName("AWS_CONTAINER_CREDENTIALS_FULL_URI")).toBe(false);
expect(isDangerousHostEnvVarName("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")).toBe(false);
});
});
describe("isDangerousHostInheritedEnvVarName", () => {
it("blocks inherited keys from both policy buckets while preserving explicit inherited allowlist keys", () => {
expect(isDangerousHostInheritedEnvVarName("BASH_ENV")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("bash_env")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("ANSIBLE_CONFIG")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("ansible_library")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("TF_CLI_CONFIG_FILE")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("TF_VAR_admin_cidr")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("AWS_CONTAINER_CREDENTIALS_FULL_URI")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")).toBe(true);
expect(isDangerousHostInheritedEnvVarName("KUBECONFIG")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("GOOGLE_APPLICATION_CREDENTIALS")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("AWS_SHARED_CREDENTIALS_FILE")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("AWS_WEB_IDENTITY_TOKEN_FILE")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("AWS_CONFIG_FILE")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("AZURE_AUTH_LOCATION")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("SSH_AUTH_SOCK")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("DOCKER_CONTEXT")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("GIT_CONFIG_GLOBAL")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("NPM_CONFIG_USERCONFIG")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("CARGO_REGISTRIES_CRATES_IO_INDEX")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("HTTP_PROXY")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("https_proxy")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("SSL_CERT_FILE")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("node_extra_ca_certs")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("HOME")).toBe(false);
expect(isDangerousHostInheritedEnvVarName("FOO")).toBe(false);
});
});
describe("sanitizeHostExecEnv", () => {
@@ -255,12 +318,14 @@ describe("sanitizeHostExecEnv", () => {
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/aws-web-token",
AZURE_AUTH_LOCATION: "/tmp/azure-auth.json",
AWS_CONFIG_FILE: "/tmp/aws-config",
SSH_AUTH_SOCK: "/tmp/trusted-ssh-agent.sock",
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_CONTEXT: "trusted-remote",
DOCKER_HOST: "tcp://docker.example.test:2376",
LD_PRELOAD: "/tmp/pwn.so",
OK: "1",
@@ -276,12 +341,12 @@ describe("sanitizeHostExecEnv", () => {
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",
SSH_AUTH_SOCK: "/tmp/trusted-ssh-agent.sock",
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_CONTEXT: "trusted-remote",
DOCKER_HOST: "tcp://docker.example.test:2376",
OK: "1",
});
@@ -428,7 +493,7 @@ describe("sanitizeHostExecEnv", () => {
expect(env.MFLAGS).toBeUndefined();
expect(env.PHPRC).toBeUndefined();
expect(env.XDG_CONFIG_HOME).toBeUndefined();
expect(env.YARN_RC_FILENAME).toBe(".trusted-yarnrc.yml");
expect(env.YARN_RC_FILENAME).toBeUndefined();
expect(env.PIP_INDEX_URL).toBeUndefined();
expect(env.PIP_PYPI_URL).toBeUndefined();
expect(env.PIP_EXTRA_INDEX_URL).toBeUndefined();
@@ -470,6 +535,110 @@ describe("sanitizeHostExecEnv", () => {
expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir");
});
it("drops inherited vars blocked by either policy bucket and keeps explicit inherited allowlist keys", () => {
const env = sanitizeHostExecEnv({
baseEnv: {
PATH: "/usr/bin:/bin",
HTTPS_PROXY: "http://trusted-proxy.example.test:8443",
KUBECONFIG: "/tmp/trusted-kubeconfig",
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/trusted-gcp.json",
AWS_SHARED_CREDENTIALS_FILE: "/tmp/trusted-aws-credentials",
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/trusted-aws-web-token",
AWS_CONFIG_FILE: "/tmp/trusted-aws-config",
AZURE_AUTH_LOCATION: "/tmp/trusted-azure-auth.json",
SSH_AUTH_SOCK: "/tmp/trusted-ssh-agent.sock",
DOCKER_CONTEXT: "trusted-remote",
VIMINIT: ":!touch /tmp/pwned",
EXINIT: "silent !touch /tmp/pwned",
LUA_INIT_5_4: "os.execute('touch /tmp/pwned')",
HOSTALIASES: "/tmp/evil-hostaliases",
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",
ANSIBLE_CONFIG: "/tmp/evil-ansible.cfg",
R_PROFILE_USER: "/tmp/evil-Rprofile",
ERL_AFLAGS: "-eval 'os:cmd(\"id\")'",
TF_CLI_CONFIG_FILE: "/tmp/evil-terraformrc",
TF_VAR_admin_cidr: "10.0.0.0/24",
SAFE: "1",
},
});
expect(env.PATH).toBe("/usr/bin:/bin");
expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE);
expect(env.VIMINIT).toBeUndefined();
expect(env.EXINIT).toBeUndefined();
expect(env.LUA_INIT_5_4).toBeUndefined();
expect(env.HOSTALIASES).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");
expect(env.AWS_SHARED_CREDENTIALS_FILE).toBe("/tmp/trusted-aws-credentials");
expect(env.AWS_WEB_IDENTITY_TOKEN_FILE).toBe("/tmp/trusted-aws-web-token");
expect(env.AWS_CONFIG_FILE).toBe("/tmp/trusted-aws-config");
expect(env.AZURE_AUTH_LOCATION).toBe("/tmp/trusted-azure-auth.json");
expect(env.SSH_AUTH_SOCK).toBe("/tmp/trusted-ssh-agent.sock");
expect(env.DOCKER_CONTEXT).toBe("trusted-remote");
expect(env.AWS_CONTAINER_CREDENTIALS_FULL_URI).toBeUndefined();
expect(env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI).toBeUndefined();
expect(env.CONFIG_SITE).toBeUndefined();
expect(env.ANSIBLE_CONFIG).toBeUndefined();
expect(env.R_PROFILE_USER).toBeUndefined();
expect(env.ERL_AFLAGS).toBeUndefined();
expect(env.TF_CLI_CONFIG_FILE).toBeUndefined();
expect(env.TF_VAR_admin_cidr).toBe("10.0.0.0/24");
expect(env.SAFE).toBe("1");
});
it("drops newly blocked override credential and startup vars", () => {
const env = sanitizeHostExecEnv({
baseEnv: {
PATH: "/usr/bin:/bin",
},
overrides: {
VIMINIT: ":!touch /tmp/pwned",
HOSTALIASES: "/tmp/evil-hostaliases",
AWS_CONTAINER_CREDENTIALS_FULL_URI: "http://attacker/credentials",
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/attacker-credentials",
ANSIBLE_CONFIG: "/tmp/override-ansible.cfg",
ANSIBLE_REMOTE_TEMP: "/tmp/evil-ansible-remote",
R_LIBS_USER: "/tmp/evil-r-libs-user",
TF_CLI_CONFIG_FILE: "/tmp/override-terraformrc",
TF_PLUGIN_CACHE_DIR: "/tmp/evil-tf-plugin-cache",
CFLAGS: "-I/attacker/include",
LDFLAGS: "-L/attacker/lib",
XDG_CONFIG_DIRS: "/tmp/evil-config-dirs",
TF_VAR_admin_cidr: "10.0.0.0/24",
GITHUB_TOKEN: "ghp-test",
DATABASE_URL: "postgres://attacker",
NPM_TOKEN: "npm-test",
SSH_AUTH_SOCK: "/tmp/evil-agent.sock",
SAFE: "ok",
},
});
expect(env.PATH).toBe("/usr/bin:/bin");
expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE);
expect(env.VIMINIT).toBeUndefined();
expect(env.HOSTALIASES).toBeUndefined();
expect(env.AWS_CONTAINER_CREDENTIALS_FULL_URI).toBeUndefined();
expect(env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI).toBeUndefined();
expect(env.ANSIBLE_CONFIG).toBeUndefined();
expect(env.ANSIBLE_REMOTE_TEMP).toBeUndefined();
expect(env.R_LIBS_USER).toBeUndefined();
expect(env.TF_CLI_CONFIG_FILE).toBeUndefined();
expect(env.TF_PLUGIN_CACHE_DIR).toBeUndefined();
expect(env.CFLAGS).toBeUndefined();
expect(env.LDFLAGS).toBeUndefined();
expect(env.XDG_CONFIG_DIRS).toBeUndefined();
expect(env.TF_VAR_admin_cidr).toBeUndefined();
expect(env.GITHUB_TOKEN).toBeUndefined();
expect(env.DATABASE_URL).toBeUndefined();
expect(env.NPM_TOKEN).toBeUndefined();
expect(env.SSH_AUTH_SOCK).toBeUndefined();
expect(env.SAFE).toBe("ok");
});
it("keeps trusted inherited proxy and TLS env while blocking overrides", () => {
const env = sanitizeHostExecEnv({
baseEnv: {
@@ -658,16 +827,53 @@ describe("isDangerousHostEnvOverrideVarName", () => {
expect(isDangerousHostEnvOverrideVarName("cargo_build_rustc_wrapper")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("CARGO_HOME")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("cargo_home")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("TF_VAR_admin_cidr")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("CORECLR_PROFILER_PATH")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("coreclr_profiler_path")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("XDG_CONFIG_HOME")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("xdg_config_home")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("XDG_CONFIG_DIRS")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("xdg_config_dirs")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("AWS_CONFIG_FILE")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("aws_config_file")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("yarn_rc_filename")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("BASH_ENV")).toBe(false);
expect(isDangerousHostEnvOverrideVarName("FOO")).toBe(false);
});
it("blocks newly added credential and build influence keys", () => {
const keys = [
"GITHUB_TOKEN",
"GH_TOKEN",
"GITLAB_TOKEN",
"NPM_TOKEN",
"NODE_AUTH_TOKEN",
"AWS_ACCESS_KEY_ID",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"ANSIBLE_CONFIG",
"ANSIBLE_LIBRARY",
"ANSIBLE_REMOTE_TEMP",
"R_LIBS_USER",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"CFLAGS",
"LDFLAGS",
"XDG_CONFIG_DIRS",
"AWS_SECRET_ACCESS_KEY",
"AZURE_CLIENT_SECRET",
"DATABASE_URL",
"REDIS_URL",
"MONGODB_URI",
"AMQP_URL",
"SSH_AUTH_SOCK",
] as const;
for (const key of keys) {
expect(isDangerousHostEnvOverrideVarName(key)).toBe(true);
expect(isDangerousHostEnvOverrideVarName(key.toLowerCase())).toBe(true);
}
});
});
describe("sanitizeHostExecEnvWithDiagnostics", () => {
@@ -886,6 +1092,65 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
expect(result.env.YARN_RC_FILENAME).toBeUndefined();
});
it("reports newly blocked keys from everywhere and override buckets", () => {
const result = sanitizeHostExecEnvWithDiagnostics({
baseEnv: {
PATH: "/usr/bin:/bin",
},
overrides: {
VIMINIT: ":!touch /tmp/pwned",
LUA_INIT_5_4: "os.execute('touch /tmp/pwned')",
HOSTALIASES: "/tmp/evil-hostaliases",
ANSIBLE_CONFIG: "/tmp/evil-ansible.cfg",
ANSIBLE_REMOTE_TEMP: "/tmp/evil-ansible-remote",
R_LIBS_USER: "/tmp/evil-r-libs-user",
TF_CLI_CONFIG_FILE: "/tmp/evil-terraformrc",
TF_PLUGIN_CACHE_DIR: "/tmp/evil-tf-plugin-cache",
AWS_CONTAINER_CREDENTIALS_FULL_URI: "http://attacker/credentials",
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: "/attacker-credentials",
GITHUB_TOKEN: "ghp-test",
DATABASE_URL: "postgres://attacker",
R_PROFILE_USER: "/tmp/evil-Rprofile",
XDG_CONFIG_DIRS: "/tmp/evil-config-dirs",
TF_VAR_admin_cidr: "10.0.0.0/24",
SAFE_KEY: "ok",
},
});
expect(result.rejectedOverrideBlockedKeys).toEqual([
"ANSIBLE_CONFIG",
"ANSIBLE_REMOTE_TEMP",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"DATABASE_URL",
"GITHUB_TOKEN",
"HOSTALIASES",
"LUA_INIT_5_4",
"R_LIBS_USER",
"R_PROFILE_USER",
"TF_CLI_CONFIG_FILE",
"TF_PLUGIN_CACHE_DIR",
"TF_VAR_ADMIN_CIDR",
"VIMINIT",
"XDG_CONFIG_DIRS",
]);
expect(result.rejectedOverrideInvalidKeys).toEqual([]);
expect(result.env.SAFE_KEY).toBe("ok");
expect(result.env.VIMINIT).toBeUndefined();
expect(result.env.LUA_INIT_5_4).toBeUndefined();
expect(result.env.HOSTALIASES).toBeUndefined();
expect(result.env.ANSIBLE_CONFIG).toBeUndefined();
expect(result.env.ANSIBLE_REMOTE_TEMP).toBeUndefined();
expect(result.env.R_LIBS_USER).toBeUndefined();
expect(result.env.TF_CLI_CONFIG_FILE).toBeUndefined();
expect(result.env.TF_PLUGIN_CACHE_DIR).toBeUndefined();
expect(result.env.GITHUB_TOKEN).toBeUndefined();
expect(result.env.DATABASE_URL).toBeUndefined();
expect(result.env.R_PROFILE_USER).toBeUndefined();
expect(result.env.XDG_CONFIG_DIRS).toBeUndefined();
expect(result.env.TF_VAR_admin_cidr).toBeUndefined();
});
it("allows Windows-style override names while still rejecting invalid keys", () => {
const result = sanitizeHostExecEnvWithDiagnostics({
baseEnv: {

View File

@@ -10,6 +10,12 @@ export const HOST_DANGEROUS_ENV_KEY_VALUES: readonly string[] = Object.freeze([
export const HOST_DANGEROUS_ENV_PREFIXES: readonly string[] = Object.freeze([
...HOST_ENV_SECURITY_POLICY.blockedPrefixes,
]);
export const HOST_DANGEROUS_INHERITED_ENV_KEY_VALUES: readonly string[] = Object.freeze([
...HOST_ENV_SECURITY_POLICY.blockedInheritedKeys,
]);
export const HOST_DANGEROUS_INHERITED_ENV_PREFIXES: readonly string[] = Object.freeze([
...HOST_ENV_SECURITY_POLICY.blockedInheritedPrefixes,
]);
export const HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze([
...HOST_ENV_SECURITY_POLICY.blockedOverrideKeys,
]);
@@ -27,6 +33,9 @@ export const HOST_SHELL_WRAPPER_ALLOWED_OVERRIDE_ENV_KEY_VALUES: readonly string
"FORCE_COLOR",
]);
export const HOST_DANGEROUS_ENV_KEYS = new Set<string>(HOST_DANGEROUS_ENV_KEY_VALUES);
export const HOST_DANGEROUS_INHERITED_ENV_KEYS = new Set<string>(
HOST_DANGEROUS_INHERITED_ENV_KEY_VALUES,
);
export const HOST_DANGEROUS_OVERRIDE_ENV_KEYS = new Set<string>(
HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES,
);
@@ -82,6 +91,18 @@ export function isDangerousHostEnvVarName(rawKey: string): boolean {
return HOST_DANGEROUS_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
}
export function isDangerousHostInheritedEnvVarName(rawKey: string): boolean {
const key = normalizeEnvVarKey(rawKey);
if (!key) {
return false;
}
const upper = key.toUpperCase();
if (HOST_DANGEROUS_INHERITED_ENV_KEYS.has(upper)) {
return true;
}
return HOST_DANGEROUS_INHERITED_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
}
export function isDangerousHostEnvOverrideVarName(rawKey: string): boolean {
const key = normalizeEnvVarKey(rawKey);
if (!key) {
@@ -178,7 +199,7 @@ export function sanitizeHostExecEnvWithDiagnostics(params?: {
const merged: Record<string, string> = {};
for (const [key, value] of listNormalizedEnvEntries(baseEnv)) {
if (isDangerousHostEnvVarName(key)) {
if (isDangerousHostInheritedEnvVarName(key)) {
continue;
}
merged[key] = value;