mirror of https://github.com/openclaw/openclaw.git
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:
parent
28bb8c600e
commit
4d912e0451
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue