fix(exec): block proxy-style env overrides (#58202)

* fix(exec): block proxy-style env overrides

* fix(exec): keep trusted host proxy env inherited

* fix(exec): block git tls override env vars

* fix(skills): block dangerous env override keys
This commit is contained in:
Vincent Koc 2026-03-31 21:25:36 +09:00 committed by GitHub
parent 28bb8c600e
commit 4d912e0451
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 187 additions and 2 deletions

View File

@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
- Agents/Kimi: preserve already-valid Anthropic-compatible tool call argument objects while still clearing cached repairs when later trailing junk exceeds the repair allowance. (#54491) Thanks @yuanaichi.
- Docker/setup: force BuildKit for local image builds (including sandbox image builds) so `./docker-setup.sh` no longer fails on `RUN --mount=...` when hosts default to Docker's legacy builder. (#56681) Thanks @zhanghui-china.
- Control UI/agents: auto-load agent workspace files on initial Files panel open, and populate overview model/workspace/fallbacks from effective runtime agent metadata so defaulted models no longer show as `Not set`. (#56637) Thanks @dxsx84.
- Exec/env: block proxy, TLS, and Docker endpoint env overrides in host execution so request-scoped commands cannot silently reroute outbound traffic or trust attacker-supplied certificate settings. Thanks @AntAISecurityLab.
- Control UI/slash commands: make `/steer` and `/redirect` work from the chat command palette with visible pending state for active-run `/steer`, correct redirected-run tracking, and a single canonical `/steer` entry in the command menu. (#54625) Thanks @fuller-stack-dev.
- Exec/approvals: keep `awk` and `sed` family binaries out of the low-risk `safeBins` fast path, and stop doctor profile scaffolding from treating them like ordinary custom filters. Thanks @vincentkoc.
- Exec/runtime: default implicit exec to `host=auto`, resolve that target to sandbox only when a sandbox runtime exists, keep explicit `host=sandbox` fail-closed without sandbox, and show `/exec` effective host state in runtime status/docs.

View File

@ -22,6 +22,9 @@ enum HostEnvSecurityPolicy {
"GIT_EXEC_PATH",
"GIT_SEQUENCE_EDITOR",
"GIT_TEMPLATE_DIR",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"CC",
"CXX",
"CARGO_BUILD_RUSTC",
@ -54,6 +57,9 @@ enum HostEnvSecurityPolicy {
"GIT_SSH",
"GIT_PROXY_COMMAND",
"GIT_ASKPASS",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"SSH_ASKPASS",
"LESSOPEN",
"LESSCLOSE",
@ -82,6 +88,19 @@ enum HostEnvSecurityPolicy {
"PHP_INI_SCAN_DIR",
"DENO_DIR",
"BUN_CONFIG_REGISTRY",
"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",
"CURL_CA_BUNDLE",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"PIP_INDEX_URL",
"PIP_PYPI_URL",
"PIP_EXTRA_INDEX_URL",

View File

@ -228,6 +228,22 @@ describe("exec host env validation", () => {
).rejects.toThrow(/Security Violation: Environment variable 'LD_DEBUG' is forbidden/);
});
it("blocks proxy and TLS override env vars on host execution", async () => {
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
await expect(
tool.execute("call1", {
command: "echo ok",
env: {
HTTPS_PROXY: "http://proxy.example.test:8080",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
},
}),
).rejects.toThrow(
/Security Violation: blocked override keys: HTTPS_PROXY, NODE_TLS_REJECT_UNAUTHORIZED\./,
);
});
it("strips dangerous inherited env vars from host execution", async () => {
if (isWin) {
return;

View File

@ -511,6 +511,50 @@ describe("applySkillEnvOverrides", () => {
});
});
it("blocks override-only host env overrides in skill config", async () => {
const workspaceDir = await makeWorkspace();
const skillDir = path.join(workspaceDir, "skills", "override-env-skill");
await writeSkill({
dir: skillDir,
name: "override-env-skill",
description: "Needs env",
metadata:
'{"openclaw":{"requires":{"env":["HTTPS_PROXY","NODE_TLS_REJECT_UNAUTHORIZED","DOCKER_HOST"]}}}',
});
const entries = loadWorkspaceSkillEntries(workspaceDir, resolveTestSkillDirs(workspaceDir));
withClearedEnv(["HTTPS_PROXY", "NODE_TLS_REJECT_UNAUTHORIZED", "DOCKER_HOST"], () => {
const restore = applySkillEnvOverrides({
skills: entries,
config: {
skills: {
entries: {
"override-env-skill": {
env: {
HTTPS_PROXY: "http://proxy.example.test:8080",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
DOCKER_HOST: "tcp://docker.example.test:2376",
},
},
},
},
},
});
try {
expect(process.env.HTTPS_PROXY).toBeUndefined();
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
expect(process.env.DOCKER_HOST).toBeUndefined();
} finally {
restore();
expect(process.env.HTTPS_PROXY).toBeUndefined();
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
expect(process.env.DOCKER_HOST).toBeUndefined();
}
});
});
it("allows required env overrides from snapshots", async () => {
const workspaceDir = await makeWorkspace();
const skillDir = path.join(workspaceDir, "skills", "snapshot-env-skill");

View File

@ -1,6 +1,9 @@
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js";
import { isDangerousHostEnvVarName } from "../../infra/host-env-security.js";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
} from "../../infra/host-env-security.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { sanitizeEnvVars, validateEnvVarValue } from "../sandbox/sanitize-env-vars.js";
import { resolveSkillConfig } from "./config.js";
@ -86,7 +89,9 @@ function matchesAnyPattern(value: string, patterns: readonly RegExp[]): boolean
function isAlwaysBlockedSkillEnvKey(key: string): boolean {
return (
isDangerousHostEnvVarName(key) || matchesAnyPattern(key, SKILL_ALWAYS_BLOCKED_ENV_PATTERNS)
isDangerousHostEnvVarName(key) ||
isDangerousHostEnvOverrideVarName(key) ||
matchesAnyPattern(key, SKILL_ALWAYS_BLOCKED_ENV_PATTERNS)
);
}

View File

@ -16,6 +16,9 @@
"GIT_EXEC_PATH",
"GIT_SEQUENCE_EDITOR",
"GIT_TEMPLATE_DIR",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"CC",
"CXX",
"CARGO_BUILD_RUSTC",
@ -47,6 +50,9 @@
"GIT_SSH",
"GIT_PROXY_COMMAND",
"GIT_ASKPASS",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"SSH_ASKPASS",
"LESSOPEN",
"LESSCLOSE",
@ -75,6 +81,19 @@
"PHP_INI_SCAN_DIR",
"DENO_DIR",
"BUN_CONFIG_REGISTRY",
"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",
"CURL_CA_BUNDLE",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"PIP_INDEX_URL",
"PIP_PYPI_URL",
"PIP_EXTRA_INDEX_URL",

View File

@ -174,6 +174,21 @@ describe("isDangerousHostEnvVarName", () => {
expect(isDangerousHostEnvVarName("gradle_opts")).toBe(true);
expect(isDangerousHostEnvVarName("ANT_OPTS")).toBe(true);
expect(isDangerousHostEnvVarName("ant_opts")).toBe(true);
expect(isDangerousHostEnvVarName("HTTPS_PROXY")).toBe(false);
expect(isDangerousHostEnvVarName("https_proxy")).toBe(false);
expect(isDangerousHostEnvVarName("HTTP_PROXY")).toBe(false);
expect(isDangerousHostEnvVarName("http_proxy")).toBe(false);
expect(isDangerousHostEnvVarName("ALL_PROXY")).toBe(false);
expect(isDangerousHostEnvVarName("no_proxy")).toBe(false);
expect(isDangerousHostEnvVarName("NODE_TLS_REJECT_UNAUTHORIZED")).toBe(false);
expect(isDangerousHostEnvVarName("node_extra_ca_certs")).toBe(false);
expect(isDangerousHostEnvVarName("SSL_CERT_FILE")).toBe(false);
expect(isDangerousHostEnvVarName("SSL_CERT_DIR")).toBe(false);
expect(isDangerousHostEnvVarName("requests_ca_bundle")).toBe(false);
expect(isDangerousHostEnvVarName("CURL_CA_BUNDLE")).toBe(false);
expect(isDangerousHostEnvVarName("DOCKER_HOST")).toBe(false);
expect(isDangerousHostEnvVarName("docker_cert_path")).toBe(false);
expect(isDangerousHostEnvVarName("DOCKER_TLS_VERIFY")).toBe(false);
expect(isDangerousHostEnvVarName("AWS_CONFIG_FILE")).toBe(false);
expect(isDangerousHostEnvVarName("aws_config_file")).toBe(false);
expect(isDangerousHostEnvVarName("PATH")).toBe(false);
@ -257,6 +272,9 @@ describe("sanitizeHostExecEnv", () => {
SSL_CERT_DIR: "/tmp/evil-cert-dir",
REQUESTS_CA_BUNDLE: "/tmp/evil-requests-ca.pem",
CURL_CA_BUNDLE: "/tmp/evil-curl-ca.pem",
GIT_SSL_NO_VERIFY: "1",
GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem",
GIT_SSL_CAPATH: "/tmp/evil-git-ca-dir",
GOPROXY: "https://example.invalid/proxy",
GONOSUMCHECK: "example.invalid/*",
GONOSUMDB: "example.invalid/*",
@ -338,6 +356,54 @@ describe("sanitizeHostExecEnv", () => {
expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir");
});
it("keeps trusted inherited proxy, TLS, and Docker env while blocking overrides", () => {
const env = sanitizeHostExecEnv({
baseEnv: {
PATH: "/usr/bin:/bin",
HTTP_PROXY: "http://trusted-proxy.example.test:8080",
HTTPS_PROXY: "http://trusted-proxy.example.test:8443",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
SSL_CERT_DIR: "/etc/ssl/certs",
CURL_CA_BUNDLE: "/etc/ssl/cert.pem",
DOCKER_TLS_VERIFY: "1",
},
overrides: {
HTTP_PROXY: "http://evil-proxy.example.test:8080",
NODE_TLS_REJECT_UNAUTHORIZED: "1",
DOCKER_TLS_VERIFY: "0",
},
});
expect(env).toEqual({
OPENCLAW_CLI: OPENCLAW_CLI_ENV_VALUE,
PATH: "/usr/bin:/bin",
HTTP_PROXY: "http://trusted-proxy.example.test:8080",
HTTPS_PROXY: "http://trusted-proxy.example.test:8443",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
SSL_CERT_DIR: "/etc/ssl/certs",
CURL_CA_BUNDLE: "/etc/ssl/cert.pem",
DOCKER_TLS_VERIFY: "1",
});
});
it("blocks proxy, TLS, and Docker override values explicitly", () => {
expect(isDangerousHostEnvOverrideVarName("HTTPS_PROXY")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("https_proxy")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("HTTP_PROXY")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("http_proxy")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("ALL_PROXY")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("no_proxy")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("NODE_TLS_REJECT_UNAUTHORIZED")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("node_extra_ca_certs")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("SSL_CERT_FILE")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("SSL_CERT_DIR")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("requests_ca_bundle")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("CURL_CA_BUNDLE")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("DOCKER_HOST")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("docker_cert_path")).toBe(true);
expect(isDangerousHostEnvOverrideVarName("DOCKER_TLS_VERIFY")).toBe(true);
});
it("drops dangerous inherited shell trace keys", () => {
const env = sanitizeHostExecEnv({
baseEnv: {
@ -503,6 +569,11 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
GOPATH: "/tmp/evil-go",
PYTHONUSERBASE: "/tmp/evil-python-userbase",
VIRTUAL_ENV: "/tmp/evil-venv",
HTTPS_PROXY: "http://proxy.example.test:8080",
GIT_SSL_NO_VERIFY: "1",
GIT_SSL_CAINFO: "/tmp/evil-git-ca.pem",
GIT_SSL_CAPATH: "/tmp/evil-git-capath",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
SAFE_KEY: "ok",
"BAD-KEY": "bad",
},
@ -520,6 +591,9 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
"DOCKER_CONTEXT",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GOENV",
"GONOPROXY",
"GONOSUMCHECK",
@ -527,8 +601,10 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
"GOPATH",
"GOPRIVATE",
"GOPROXY",
"HTTPS_PROXY",
"LIBRARY_PATH",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"OBJC_INCLUDE_PATH",
"PATH",
"PIP_CONFIG_FILE",
@ -563,6 +639,9 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
expect(result.env.UV_INDEX_URL).toBeUndefined();
expect(result.env.UV_DEFAULT_INDEX).toBeUndefined();
expect(result.env.UV_EXTRA_INDEX_URL).toBeUndefined();
expect(result.env.GIT_SSL_NO_VERIFY).toBeUndefined();
expect(result.env.GIT_SSL_CAINFO).toBeUndefined();
expect(result.env.GIT_SSL_CAPATH).toBeUndefined();
expect(result.env.DOCKER_HOST).toBeUndefined();
expect(result.env.DOCKER_TLS_VERIFY).toBeUndefined();
expect(result.env.DOCKER_CERT_PATH).toBeUndefined();
@ -584,6 +663,8 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => {
expect(result.env.GOPRIVATE).toBeUndefined();
expect(result.env.GOENV).toBeUndefined();
expect(result.env.GOPATH).toBeUndefined();
expect(result.env.HTTPS_PROXY).toBeUndefined();
expect(result.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
expect(result.env.PYTHONUSERBASE).toBeUndefined();
expect(result.env.VIRTUAL_ENV).toBeUndefined();
});