mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(env): align inherited host exec env filtering (#59119)
* fix(env): block inherited host exec config vars * fix(env): preserve trusted inherited proxy env * fix(env): preserve inherited host exec vars * fix(env): refresh host env policy parity artifacts * test(env): align blocked override ordering * docs(changelog): add host env policy parity entry --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
@@ -99,6 +99,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk.
|
||||
- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.
|
||||
- Git/env sanitization: block additional Git repository-plumbing env variables such as `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, and `GIT_NAMESPACE` so host-run Git commands cannot be redirected to attacker-chosen repository state through inherited or request-scoped env. (#62002) Thanks @eleqtrizit.
|
||||
- Host exec/env sanitization: block additional request-scoped credential and config-path overrides such as `KUBECONFIG`, cloud credential-path env, `CARGO_HOME`, and `HELM_HOME` so host-run tools can no longer be redirected to attacker-chosen config or state. (#59119) Thanks @eleqtrizit.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
|
||||
@@ -6,182 +6,180 @@ import Foundation
|
||||
|
||||
enum HostEnvSecurityPolicy {
|
||||
static let blockedKeys: Set<String> = [
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"PERL5LIB",
|
||||
"PERL5OPT",
|
||||
"RUBYLIB",
|
||||
"RUBYOPT",
|
||||
"_JAVA_OPTIONS",
|
||||
"ANT_OPTS",
|
||||
"BASH_ENV",
|
||||
"ENV",
|
||||
"BROWSER",
|
||||
"GIT_EDITOR",
|
||||
"GIT_EXTERNAL_DIFF",
|
||||
"GIT_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_EXEC_PATH",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_SEQUENCE_EDITOR",
|
||||
"GIT_TEMPLATE_DIR",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"CC",
|
||||
"CXX",
|
||||
"CARGO_BUILD_RUSTC",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"RUSTC_WRAPPER",
|
||||
"CC",
|
||||
"CMAKE_C_COMPILER",
|
||||
"CMAKE_CXX_COMPILER",
|
||||
"SHELL",
|
||||
"SHELLOPTS",
|
||||
"PS4",
|
||||
"CXX",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"ENV",
|
||||
"GCONV_PATH",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_DIR",
|
||||
"GIT_EDITOR",
|
||||
"GIT_EXEC_PATH",
|
||||
"GIT_EXTERNAL_DIFF",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_SEQUENCE_EDITOR",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_TEMPLATE_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GLIBC_TUNABLES",
|
||||
"GRADLE_OPTS",
|
||||
"HGRCPATH",
|
||||
"IFS",
|
||||
"SSLKEYLOGFILE",
|
||||
"JAVA_OPTS",
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
"_JAVA_OPTIONS",
|
||||
"JDK_JAVA_OPTIONS",
|
||||
"PYTHONBREAKPOINT",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"GLIBC_TUNABLES",
|
||||
"MAVEN_OPTS",
|
||||
"MAKEFLAGS",
|
||||
"MAVEN_OPTS",
|
||||
"MFLAGS",
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"PERL5LIB",
|
||||
"PERL5OPT",
|
||||
"PS4",
|
||||
"PYTHONBREAKPOINT",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"RUBYLIB",
|
||||
"RUBYOPT",
|
||||
"RUSTC_WRAPPER",
|
||||
"SBT_OPTS",
|
||||
"GRADLE_OPTS",
|
||||
"ANT_OPTS",
|
||||
"HGRCPATH"
|
||||
"SHELL",
|
||||
"SHELLOPTS",
|
||||
"SSLKEYLOGFILE"
|
||||
]
|
||||
|
||||
static let blockedOverrideKeys: Set<String> = [
|
||||
"HOME",
|
||||
"GRADLE_USER_HOME",
|
||||
"ZDOTDIR",
|
||||
"GIT_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSH",
|
||||
"GIT_PROXY_COMMAND",
|
||||
"GIT_ASKPASS",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"SSH_ASKPASS",
|
||||
"LESSOPEN",
|
||||
"LESSCLOSE",
|
||||
"PAGER",
|
||||
"MANPAGER",
|
||||
"GIT_PAGER",
|
||||
"EDITOR",
|
||||
"VISUAL",
|
||||
"FCEDIT",
|
||||
"SUDO_EDITOR",
|
||||
"PROMPT_COMMAND",
|
||||
"HISTFILE",
|
||||
"PERL5DB",
|
||||
"PERL5DBCMD",
|
||||
"OPENSSL_CONF",
|
||||
"OPENSSL_ENGINES",
|
||||
"PYTHONSTARTUP",
|
||||
"WGETRC",
|
||||
"CURL_HOME",
|
||||
"CLASSPATH",
|
||||
"ALL_PROXY",
|
||||
"AWS_CONFIG_FILE",
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AZURE_AUTH_LOCATION",
|
||||
"BUN_CONFIG_REGISTRY",
|
||||
"BUNDLE_GEMFILE",
|
||||
"C_INCLUDE_PATH",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"CARGO_HOME",
|
||||
"CGO_CFLAGS",
|
||||
"CGO_LDFLAGS",
|
||||
"GOFLAGS",
|
||||
"MAKEFLAGS",
|
||||
"MFLAGS",
|
||||
"CLASSPATH",
|
||||
"COMPOSER_HOME",
|
||||
"CORECLR_PROFILER_PATH",
|
||||
"PHPRC",
|
||||
"PHP_INI_SCAN_DIR",
|
||||
"DENO_DIR",
|
||||
"BUN_CONFIG_REGISTRY",
|
||||
"YARN_RC_FILENAME",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"NO_PROXY",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"CPATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"CURL_CA_BUNDLE",
|
||||
"CURL_HOME",
|
||||
"DENO_DIR",
|
||||
"DOCKER_CERT_PATH",
|
||||
"DOCKER_CONTEXT",
|
||||
"DOCKER_HOST",
|
||||
"DOCKER_TLS_VERIFY",
|
||||
"DOCKER_CERT_PATH",
|
||||
"EDITOR",
|
||||
"FCEDIT",
|
||||
"GEM_HOME",
|
||||
"GEM_PATH",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_ASKPASS",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_DIR",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_PAGER",
|
||||
"GIT_PROXY_COMMAND",
|
||||
"GIT_SSH",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_WORK_TREE",
|
||||
"GOENV",
|
||||
"GOFLAGS",
|
||||
"GONOPROXY",
|
||||
"GONOSUMCHECK",
|
||||
"GONOSUMDB",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GOPATH",
|
||||
"GOPRIVATE",
|
||||
"GOPROXY",
|
||||
"GRADLE_USER_HOME",
|
||||
"HELM_HOME",
|
||||
"HGRCPATH",
|
||||
"HISTFILE",
|
||||
"HOME",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"KUBECONFIG",
|
||||
"LESSCLOSE",
|
||||
"LESSOPEN",
|
||||
"LIBRARY_PATH",
|
||||
"LUA_CPATH",
|
||||
"LUA_PATH",
|
||||
"MAKEFLAGS",
|
||||
"MANPAGER",
|
||||
"MFLAGS",
|
||||
"NO_PROXY",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OPENSSL_CONF",
|
||||
"OPENSSL_ENGINES",
|
||||
"PAGER",
|
||||
"PERL5DB",
|
||||
"PERL5DBCMD",
|
||||
"PHP_INI_SCAN_DIR",
|
||||
"PHPRC",
|
||||
"PIP_CONFIG_FILE",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_FIND_LINKS",
|
||||
"PIP_INDEX_URL",
|
||||
"PIP_PYPI_URL",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_CONFIG_FILE",
|
||||
"PIP_FIND_LINKS",
|
||||
"PIP_TRUSTED_HOST",
|
||||
"PROMPT_COMMAND",
|
||||
"PYTHONSTARTUP",
|
||||
"PYTHONUSERBASE",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"RUSTC_WRAPPER",
|
||||
"RUSTFLAGS",
|
||||
"SSH_ASKPASS",
|
||||
"SSL_CERT_DIR",
|
||||
"SSL_CERT_FILE",
|
||||
"SUDO_EDITOR",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_INDEX",
|
||||
"UV_INDEX_URL",
|
||||
"UV_PYTHON",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"DOCKER_HOST",
|
||||
"DOCKER_TLS_VERIFY",
|
||||
"DOCKER_CERT_PATH",
|
||||
"DOCKER_CONTEXT",
|
||||
"LIBRARY_PATH",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"CURL_CA_BUNDLE",
|
||||
"GOPROXY",
|
||||
"GONOSUMCHECK",
|
||||
"GONOSUMDB",
|
||||
"GONOPROXY",
|
||||
"GOPRIVATE",
|
||||
"GOENV",
|
||||
"GOPATH",
|
||||
"HGRCPATH",
|
||||
"PYTHONUSERBASE",
|
||||
"RUSTC_WRAPPER",
|
||||
"RUSTFLAGS",
|
||||
"CARGO_HOME",
|
||||
"VIRTUAL_ENV",
|
||||
"LUA_PATH",
|
||||
"LUA_CPATH",
|
||||
"GEM_HOME",
|
||||
"GEM_PATH",
|
||||
"BUNDLE_GEMFILE",
|
||||
"COMPOSER_HOME",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"VISUAL",
|
||||
"WGETRC",
|
||||
"XDG_CONFIG_HOME",
|
||||
"AWS_CONFIG_FILE"
|
||||
"YARN_RC_FILENAME",
|
||||
"ZDOTDIR"
|
||||
]
|
||||
|
||||
static let blockedOverridePrefixes: [String] = [
|
||||
"CARGO_REGISTRIES_",
|
||||
"GIT_CONFIG_",
|
||||
"NPM_CONFIG_",
|
||||
"CARGO_REGISTRIES_"
|
||||
"NPM_CONFIG_"
|
||||
]
|
||||
|
||||
static let blockedPrefixes: [String] = [
|
||||
"BASH_FUNC_",
|
||||
"DYLD_",
|
||||
"LD_",
|
||||
"BASH_FUNC_"
|
||||
"LD_"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadHostEnvSecurityPolicy } from "../src/infra/host-env-security-policy.js";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const checkOnly = args.has("--check");
|
||||
@@ -24,8 +25,8 @@ const outputPath = path.join(
|
||||
"HostEnvSecurityPolicy.generated.swift",
|
||||
);
|
||||
|
||||
/** @type {{blockedKeys: string[]; blockedOverrideKeys?: string[]; blockedOverridePrefixes?: string[]; blockedPrefixes: string[]}} */
|
||||
const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
||||
const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
||||
const policy = loadHostEnvSecurityPolicy(rawPolicy);
|
||||
|
||||
const renderSwiftStringArray = (items) => items.map((item) => ` "${item}"`).join(",\n");
|
||||
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type HostEnvSecurityPolicy = Readonly<{
|
||||
blockedEverywhereKeys: readonly string[];
|
||||
blockedOverrideOnlyKeys: readonly string[];
|
||||
blockedPrefixes: readonly string[];
|
||||
blockedOverridePrefixes: readonly string[];
|
||||
blockedKeys: readonly string[];
|
||||
blockedOverrideKeys: readonly string[];
|
||||
}>;
|
||||
|
||||
export declare function loadHostEnvSecurityPolicy(
|
||||
rawPolicy?: Partial<HostEnvSecurityPolicy>,
|
||||
): HostEnvSecurityPolicy;
|
||||
|
||||
export declare const HOST_ENV_SECURITY_POLICY: HostEnvSecurityPolicy;
|
||||
@@ -0,0 +1,35 @@
|
||||
import HOST_ENV_SECURITY_POLICY_JSON from "./host-env-security-policy.json" with { type: "json" };
|
||||
|
||||
function sortUniqueUppercase(values) {
|
||||
return Object.freeze(
|
||||
Array.from(new Set(values.map((value) => value.toUpperCase()))).toSorted((a, b) =>
|
||||
a.localeCompare(b),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function derivePolicyArrays(policy) {
|
||||
const blockedEverywhereKeys = policy.blockedEverywhereKeys ?? [];
|
||||
const blockedOverrideOnlyKeys = policy.blockedOverrideOnlyKeys ?? [];
|
||||
|
||||
return {
|
||||
blockedKeys: sortUniqueUppercase(blockedEverywhereKeys),
|
||||
blockedOverrideKeys: sortUniqueUppercase(blockedOverrideOnlyKeys),
|
||||
blockedPrefixes: sortUniqueUppercase(policy.blockedPrefixes ?? []),
|
||||
blockedOverridePrefixes: sortUniqueUppercase(policy.blockedOverridePrefixes ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
export function loadHostEnvSecurityPolicy(rawPolicy = HOST_ENV_SECURITY_POLICY_JSON) {
|
||||
const derived = derivePolicyArrays(rawPolicy);
|
||||
return Object.freeze({
|
||||
blockedEverywhereKeys: Object.freeze(rawPolicy.blockedEverywhereKeys ?? []),
|
||||
blockedOverrideOnlyKeys: Object.freeze(rawPolicy.blockedOverrideOnlyKeys ?? []),
|
||||
blockedPrefixes: derived.blockedPrefixes,
|
||||
blockedOverridePrefixes: derived.blockedOverridePrefixes,
|
||||
blockedKeys: derived.blockedKeys,
|
||||
blockedOverrideKeys: derived.blockedOverrideKeys,
|
||||
});
|
||||
}
|
||||
|
||||
export const HOST_ENV_SECURITY_POLICY = loadHostEnvSecurityPolicy();
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"blockedKeys": [
|
||||
"blockedEverywhereKeys": [
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"PYTHONHOME",
|
||||
@@ -55,7 +55,7 @@
|
||||
"ANT_OPTS",
|
||||
"HGRCPATH"
|
||||
],
|
||||
"blockedOverrideKeys": [
|
||||
"blockedOverrideOnlyKeys": [
|
||||
"HOME",
|
||||
"GRADLE_USER_HOME",
|
||||
"ZDOTDIR",
|
||||
@@ -128,9 +128,6 @@
|
||||
"UV_PYTHON",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"DOCKER_HOST",
|
||||
"DOCKER_TLS_VERIFY",
|
||||
"DOCKER_CERT_PATH",
|
||||
"DOCKER_CONTEXT",
|
||||
"LIBRARY_PATH",
|
||||
"CPATH",
|
||||
@@ -163,7 +160,14 @@
|
||||
"COMPOSER_HOME",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"XDG_CONFIG_HOME",
|
||||
"AWS_CONFIG_FILE"
|
||||
"AWS_CONFIG_FILE",
|
||||
"KUBECONFIG",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AZURE_AUTH_LOCATION",
|
||||
"CARGO_HOME",
|
||||
"HELM_HOME"
|
||||
],
|
||||
"blockedOverridePrefixes": ["GIT_CONFIG_", "NPM_CONFIG_", "CARGO_REGISTRIES_"],
|
||||
"blockedPrefixes": ["DYLD_", "LD_", "BASH_FUNC_"]
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type HostEnvSecurityPolicy = {
|
||||
blockedKeys: string[];
|
||||
blockedOverrideKeys?: string[];
|
||||
blockedOverridePrefixes?: string[];
|
||||
blockedPrefixes: string[];
|
||||
};
|
||||
import { loadHostEnvSecurityPolicy } from "./host-env-security-policy.js";
|
||||
|
||||
function parseSwiftStringArray(source: string, marker: string): string[] {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
@@ -19,6 +13,10 @@ function parseSwiftStringArray(source: string, marker: string): string[] {
|
||||
return Array.from(match[1].matchAll(/"([^"]+)"/g), (m) => m[1]);
|
||||
}
|
||||
|
||||
function sortUnique(values: string[]): string[] {
|
||||
return Array.from(new Set(values)).toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
describe("host env security policy parity", () => {
|
||||
it("keeps generated macOS host env policy in sync with shared JSON policy", () => {
|
||||
const repoRoot = process.cwd();
|
||||
@@ -32,7 +30,8 @@ describe("host env security policy parity", () => {
|
||||
"apps/macos/Sources/OpenClaw/HostEnvSanitizer.swift",
|
||||
);
|
||||
|
||||
const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")) as HostEnvSecurityPolicy;
|
||||
const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
||||
const policy = loadHostEnvSecurityPolicy(rawPolicy);
|
||||
const generatedSource = fs.readFileSync(generatedSwiftPath, "utf8");
|
||||
const sanitizerSource = fs.readFileSync(sanitizerSwiftPath, "utf8");
|
||||
|
||||
@@ -68,4 +67,14 @@ describe("host env security policy parity", () => {
|
||||
"private static let blockedPrefixes = HostEnvSecurityPolicy.blockedPrefixes",
|
||||
);
|
||||
});
|
||||
|
||||
it("derives inherited and override lists from explicit policy buckets", () => {
|
||||
const repoRoot = process.cwd();
|
||||
const policyPath = path.join(repoRoot, "src/infra/host-env-security-policy.json");
|
||||
const rawPolicy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
||||
const policy = loadHostEnvSecurityPolicy(rawPolicy);
|
||||
|
||||
expect(policy.blockedKeys).toEqual(sortUnique([...policy.blockedEverywhereKeys]));
|
||||
expect(policy.blockedOverrideKeys).toEqual(sortUnique([...policy.blockedOverrideOnlyKeys]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,18 +146,25 @@ describe("isDangerousHostEnvVarName", () => {
|
||||
expect(isDangerousHostEnvVarName("git_sequence_editor")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("GIT_TEMPLATE_DIR")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("git_template_dir")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("KUBECONFIG")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("google_application_credentials")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("AWS_SHARED_CREDENTIALS_FILE")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("aws_web_identity_token_file")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("AZURE_AUTH_LOCATION")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("CC")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cxx")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("CARGO_BUILD_RUSTC")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cargo_build_rustc")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("CARGO_BUILD_RUSTC_WRAPPER")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cargo_build_rustc_wrapper")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cargo_home")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("CMAKE_C_COMPILER")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cmake_c_compiler")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("CMAKE_CXX_COMPILER")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("cmake_cxx_compiler")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("RUSTC_WRAPPER")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("rustc_wrapper")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("HELM_HOME")).toBe(false);
|
||||
expect(isDangerousHostEnvVarName("SHELLOPTS")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("ps4")).toBe(true);
|
||||
expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true);
|
||||
@@ -242,7 +249,19 @@ describe("sanitizeHostExecEnv", () => {
|
||||
JAVA_OPTS: "-javaagent:/tmp/evil.jar",
|
||||
MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)",
|
||||
MFLAGS: "--eval=$(shell touch /tmp/pwned-too)",
|
||||
KUBECONFIG: "/tmp/kubeconfig",
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp.json",
|
||||
AWS_SHARED_CREDENTIALS_FILE: "/tmp/aws-credentials",
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/aws-web-token",
|
||||
AZURE_AUTH_LOCATION: "/tmp/azure-auth.json",
|
||||
AWS_CONFIG_FILE: "/tmp/aws-config",
|
||||
CARGO_HOME: "/tmp/cargo",
|
||||
HELM_HOME: "/tmp/helm",
|
||||
HTTP_PROXY: "http://proxy.example.test:8080",
|
||||
HTTPS_PROXY: "http://proxy.example.test:8443",
|
||||
SSL_CERT_FILE: "/tmp/evil-cert.pem",
|
||||
SSL_CERT_DIR: "/tmp/evil-cert-dir",
|
||||
DOCKER_HOST: "tcp://docker.example.test:2376",
|
||||
LD_PRELOAD: "/tmp/pwn.so",
|
||||
OK: "1",
|
||||
},
|
||||
@@ -252,6 +271,18 @@ describe("sanitizeHostExecEnv", () => {
|
||||
OPENCLAW_CLI: OPENCLAW_CLI_ENV_VALUE,
|
||||
PATH: "/usr/bin:/bin",
|
||||
AWS_CONFIG_FILE: "/tmp/aws-config",
|
||||
KUBECONFIG: "/tmp/kubeconfig",
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/gcp.json",
|
||||
AWS_SHARED_CREDENTIALS_FILE: "/tmp/aws-credentials",
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/aws-web-token",
|
||||
AZURE_AUTH_LOCATION: "/tmp/azure-auth.json",
|
||||
CARGO_HOME: "/tmp/cargo",
|
||||
HELM_HOME: "/tmp/helm",
|
||||
HTTP_PROXY: "http://proxy.example.test:8080",
|
||||
HTTPS_PROXY: "http://proxy.example.test:8443",
|
||||
SSL_CERT_FILE: "/tmp/evil-cert.pem",
|
||||
SSL_CERT_DIR: "/tmp/evil-cert-dir",
|
||||
DOCKER_HOST: "tcp://docker.example.test:2376",
|
||||
OK: "1",
|
||||
});
|
||||
});
|
||||
@@ -296,6 +327,11 @@ describe("sanitizeHostExecEnv", () => {
|
||||
CARGO_REGISTRIES_CRATES_IO_INDEX: "https://example.invalid/crates.io-index",
|
||||
AWS_CONFIG_FILE: "/tmp/override-aws-config",
|
||||
YARN_RC_FILENAME: ".evil-yarnrc.yml",
|
||||
KUBECONFIG: "/tmp/override-kubeconfig",
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/override-gcp.json",
|
||||
AWS_SHARED_CREDENTIALS_FILE: "/tmp/override-aws-credentials",
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/override-aws-web-token",
|
||||
AZURE_AUTH_LOCATION: "/tmp/override-azure-auth.json",
|
||||
PIP_INDEX_URL: "https://example.invalid/simple",
|
||||
PIP_PYPI_URL: "https://example.invalid/simple",
|
||||
PIP_EXTRA_INDEX_URL: "https://example.invalid/simple",
|
||||
@@ -316,6 +352,8 @@ describe("sanitizeHostExecEnv", () => {
|
||||
C_INCLUDE_PATH: "/tmp/evil-c-headers",
|
||||
CPLUS_INCLUDE_PATH: "/tmp/evil-cpp-headers",
|
||||
OBJC_INCLUDE_PATH: "/tmp/evil-objc-headers",
|
||||
CARGO_HOME: "/tmp/override-cargo",
|
||||
HELM_HOME: "/tmp/override-helm",
|
||||
NODE_EXTRA_CA_CERTS: "/tmp/evil-ca.pem",
|
||||
SSL_CERT_FILE: "/tmp/evil-cert.pem",
|
||||
SSL_CERT_DIR: "/tmp/evil-cert-dir",
|
||||
@@ -371,6 +409,11 @@ describe("sanitizeHostExecEnv", () => {
|
||||
expect(env.GIT_NAMESPACE).toBeUndefined();
|
||||
expect(env.GIT_SEQUENCE_EDITOR).toBeUndefined();
|
||||
expect(env.AWS_CONFIG_FILE).toBeUndefined();
|
||||
expect(env.KUBECONFIG).toBeUndefined();
|
||||
expect(env.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined();
|
||||
expect(env.AWS_SHARED_CREDENTIALS_FILE).toBeUndefined();
|
||||
expect(env.AWS_WEB_IDENTITY_TOKEN_FILE).toBeUndefined();
|
||||
expect(env.AZURE_AUTH_LOCATION).toBeUndefined();
|
||||
expect(env.GIT_SSH_COMMAND).toBeUndefined();
|
||||
expect(env.GIT_EXEC_PATH).toBeUndefined();
|
||||
expect(env.EDITOR).toBeUndefined();
|
||||
@@ -421,6 +464,7 @@ describe("sanitizeHostExecEnv", () => {
|
||||
expect(env.GOENV).toBeUndefined();
|
||||
expect(env.GOPATH).toBeUndefined();
|
||||
expect(env.CARGO_HOME).toBeUndefined();
|
||||
expect(env.HELM_HOME).toBeUndefined();
|
||||
expect(env.PYTHONUSERBASE).toBeUndefined();
|
||||
expect(env.VIRTUAL_ENV).toBeUndefined();
|
||||
expect(env.SAFE).toBe("ok");
|
||||
@@ -428,7 +472,7 @@ describe("sanitizeHostExecEnv", () => {
|
||||
expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir");
|
||||
});
|
||||
|
||||
it("keeps trusted inherited proxy, TLS, and Docker env while blocking overrides", () => {
|
||||
it("keeps trusted inherited proxy and TLS env while blocking overrides", () => {
|
||||
const env = sanitizeHostExecEnv({
|
||||
baseEnv: {
|
||||
PATH: "/usr/bin:/bin",
|
||||
@@ -591,6 +635,13 @@ describe("isDangerousHostEnvOverrideVarName", () => {
|
||||
expect(isDangerousHostEnvOverrideVarName("goenv")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("PYTHONUSERBASE")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("virtual_env")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("KUBECONFIG")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("google_application_credentials")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("AWS_SHARED_CREDENTIALS_FILE")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("aws_web_identity_token_file")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("AZURE_AUTH_LOCATION")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("cargo_home")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("HELM_HOME")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("CLASSPATH")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("classpath")).toBe(true);
|
||||
expect(isDangerousHostEnvOverrideVarName("MAKEFLAGS")).toBe(true);
|
||||
@@ -633,6 +684,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
CARGO_BUILD_RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper",
|
||||
CARGO_REGISTRIES_CRATES_IO_INDEX: "https://example.invalid/crates.io-index",
|
||||
CMAKE_C_COMPILER: "/tmp/evil-c-compiler",
|
||||
KUBECONFIG: "/tmp/evil-kubeconfig",
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/evil-gcp.json",
|
||||
AWS_SHARED_CREDENTIALS_FILE: "/tmp/evil-aws-credentials",
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/evil-aws-web-token",
|
||||
AZURE_AUTH_LOCATION: "/tmp/evil-azure-auth.json",
|
||||
CLASSPATH: "/tmp/evil-classpath",
|
||||
PIP_INDEX_URL: "https://example.invalid/simple",
|
||||
PIP_PYPI_URL: "https://example.invalid/simple",
|
||||
@@ -677,6 +733,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
HGRCPATH: "/tmp/evil-hgrc",
|
||||
MAKEFLAGS: "--eval=$(shell touch /tmp/pwned)",
|
||||
MFLAGS: "--eval=$(shell touch /tmp/pwned-too)",
|
||||
HELM_HOME: "/tmp/evil-helm",
|
||||
PYTHONUSERBASE: "/tmp/evil-python-userbase",
|
||||
RUSTC_WRAPPER: "/tmp/evil-rustc-wrapper",
|
||||
RUSTFLAGS: "-C link-args=-l/tmp/evil.so",
|
||||
@@ -694,6 +751,9 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
});
|
||||
|
||||
expect(result.rejectedOverrideBlockedKeys).toEqual([
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AZURE_AUTH_LOCATION",
|
||||
"C_INCLUDE_PATH",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"CARGO_HOME",
|
||||
@@ -722,12 +782,15 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
"GONOPROXY",
|
||||
"GONOSUMCHECK",
|
||||
"GONOSUMDB",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GOPATH",
|
||||
"GOPRIVATE",
|
||||
"GOPROXY",
|
||||
"HELM_HOME",
|
||||
"HGRCPATH",
|
||||
"HTTPS_PROXY",
|
||||
"JAVA_OPTS",
|
||||
"KUBECONFIG",
|
||||
"LIBRARY_PATH",
|
||||
"MAKEFLAGS",
|
||||
"MFLAGS",
|
||||
@@ -774,6 +837,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
expect(result.env.UV_PYTHON).toBeUndefined();
|
||||
expect(result.env.UV_DEFAULT_INDEX).toBeUndefined();
|
||||
expect(result.env.UV_EXTRA_INDEX_URL).toBeUndefined();
|
||||
expect(result.env.KUBECONFIG).toBeUndefined();
|
||||
expect(result.env.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined();
|
||||
expect(result.env.AWS_SHARED_CREDENTIALS_FILE).toBeUndefined();
|
||||
expect(result.env.AWS_WEB_IDENTITY_TOKEN_FILE).toBeUndefined();
|
||||
expect(result.env.AZURE_AUTH_LOCATION).toBeUndefined();
|
||||
expect(result.env.GIT_SSL_NO_VERIFY).toBeUndefined();
|
||||
expect(result.env.GIT_SSL_CAINFO).toBeUndefined();
|
||||
expect(result.env.GIT_SSL_CAPATH).toBeUndefined();
|
||||
@@ -807,6 +875,7 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
|
||||
expect(result.env.GOPATH).toBeUndefined();
|
||||
expect(result.env.CARGO_HOME).toBeUndefined();
|
||||
expect(result.env.HGRCPATH).toBeUndefined();
|
||||
expect(result.env.HELM_HOME).toBeUndefined();
|
||||
expect(result.env.HTTPS_PROXY).toBeUndefined();
|
||||
expect(result.env.JAVA_OPTS).toBeUndefined();
|
||||
expect(result.env.MAKEFLAGS).toBeUndefined();
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
import HOST_ENV_SECURITY_POLICY_JSON from "./host-env-security-policy.json" with { type: "json" };
|
||||
import { HOST_ENV_SECURITY_POLICY } from "./host-env-security-policy.js";
|
||||
import { markOpenClawExecEnv } from "./openclaw-exec-env.js";
|
||||
|
||||
const PORTABLE_ENV_VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
const WINDOWS_COMPAT_OVERRIDE_ENV_VAR_KEY = /^[A-Za-z_][A-Za-z0-9_()]*$/;
|
||||
|
||||
type HostEnvSecurityPolicy = {
|
||||
blockedKeys: string[];
|
||||
blockedOverrideKeys?: string[];
|
||||
blockedOverridePrefixes?: string[];
|
||||
blockedPrefixes: string[];
|
||||
};
|
||||
|
||||
const HOST_ENV_SECURITY_POLICY = HOST_ENV_SECURITY_POLICY_JSON as HostEnvSecurityPolicy;
|
||||
|
||||
export const HOST_DANGEROUS_ENV_KEY_VALUES: readonly string[] = Object.freeze(
|
||||
HOST_ENV_SECURITY_POLICY.blockedKeys.map((key) => key.toUpperCase()),
|
||||
);
|
||||
export const HOST_DANGEROUS_ENV_PREFIXES: readonly string[] = Object.freeze(
|
||||
HOST_ENV_SECURITY_POLICY.blockedPrefixes.map((prefix) => prefix.toUpperCase()),
|
||||
);
|
||||
export const HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze(
|
||||
(HOST_ENV_SECURITY_POLICY.blockedOverrideKeys ?? []).map((key) => key.toUpperCase()),
|
||||
);
|
||||
export const HOST_DANGEROUS_OVERRIDE_ENV_PREFIXES: readonly string[] = Object.freeze(
|
||||
(HOST_ENV_SECURITY_POLICY.blockedOverridePrefixes ?? []).map((prefix) => prefix.toUpperCase()),
|
||||
);
|
||||
export const HOST_DANGEROUS_ENV_KEY_VALUES: readonly string[] = Object.freeze([
|
||||
...HOST_ENV_SECURITY_POLICY.blockedKeys,
|
||||
]);
|
||||
export const HOST_DANGEROUS_ENV_PREFIXES: readonly string[] = Object.freeze([
|
||||
...HOST_ENV_SECURITY_POLICY.blockedPrefixes,
|
||||
]);
|
||||
export const HOST_DANGEROUS_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze([
|
||||
...HOST_ENV_SECURITY_POLICY.blockedOverrideKeys,
|
||||
]);
|
||||
export const HOST_DANGEROUS_OVERRIDE_ENV_PREFIXES: readonly string[] = Object.freeze([
|
||||
...HOST_ENV_SECURITY_POLICY.blockedOverridePrefixes,
|
||||
]);
|
||||
export const HOST_SHELL_WRAPPER_ALLOWED_OVERRIDE_ENV_KEY_VALUES: readonly string[] = Object.freeze([
|
||||
"TERM",
|
||||
"LANG",
|
||||
|
||||
Reference in New Issue
Block a user