mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
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:
committed by
GitHub
parent
71617ef2f0
commit
2d126fc623
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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] = [
|
||||
|
||||
@@ -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)}
|
||||
]
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
|
||||
3
src/infra/host-env-security-policy.d.ts
vendored
3
src/infra/host-env-security-policy.d.ts
vendored
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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_"]
|
||||
}
|
||||
|
||||
@@ -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 ?? []),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
241
src/infra/host-env-security.reported-baseline.json
Normal file
241
src/infra/host-env-security.reported-baseline.json
Normal 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
|
||||
}
|
||||
180
src/infra/host-env-security.reported-baseline.test.ts
Normal file
180
src/infra/host-env-security.reported-baseline.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user