diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f5b60f965..d10f0100792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai - Doctor/plugins: skip false Matrix legacy-helper warnings when no migration plans exist, and keep bundled `enabledByDefault` plugins in the gateway startup set. (#57931) Thanks @dinakars777. - Matrix/CLI send: start one-off Matrix send clients before outbound delivery so `openclaw message send --channel matrix` restores E2EE in encrypted rooms instead of sending plain events. (#57936) Thanks @gumadeiras. - Matrix/direct rooms: stop trusting remote `is_direct`, honor explicit local `is_direct: false` for discovered DM candidates, and avoid extra member-state lookups for shared rooms so DM routing and repair stay aligned. (#57124) Thanks @w-sss. +- Exec/env: block Python package index override variables from request-scoped host exec environment sanitization so package fetches cannot be redirected through a caller-supplied index. Thanks @nexrin and @vincentkoc. ## 2026.3.28 diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index 5a3edde6a83..3a7789551bf 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -82,6 +82,13 @@ enum HostEnvSecurityPolicy { "PHP_INI_SCAN_DIR", "DENO_DIR", "BUN_CONFIG_REGISTRY", + "PIP_INDEX_URL", + "PIP_PYPI_URL", + "PIP_EXTRA_INDEX_URL", + "UV_INDEX", + "UV_INDEX_URL", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", "LUA_PATH", "LUA_CPATH", "GEM_HOME", diff --git a/src/infra/host-env-security-policy.json b/src/infra/host-env-security-policy.json index 10459e2adb7..aa9435cbd17 100644 --- a/src/infra/host-env-security-policy.json +++ b/src/infra/host-env-security-policy.json @@ -75,6 +75,14 @@ "PHP_INI_SCAN_DIR", "DENO_DIR", "BUN_CONFIG_REGISTRY", + "PIP_INDEX_URL", + "PIP_PYPI_URL", + "PIP_EXTRA_INDEX_URL", + "UV_INDEX", + "UV_INDEX_URL", + "UV_EXTRA_INDEX_URL", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", "LUA_PATH", "LUA_CPATH", "GEM_HOME", diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 6788dd7546f..85c7687dace 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -233,6 +233,13 @@ describe("sanitizeHostExecEnv", () => { NPM_CONFIG_USERCONFIG: "/tmp/npmrc", GIT_CONFIG_GLOBAL: "/tmp/gitconfig", AWS_CONFIG_FILE: "/tmp/override-aws-config", + PIP_INDEX_URL: "https://example.invalid/simple", + PIP_PYPI_URL: "https://example.invalid/simple", + PIP_EXTRA_INDEX_URL: "https://example.invalid/simple", + UV_INDEX: "https://example.invalid/simple", + UV_INDEX_URL: "https://example.invalid/simple", + UV_DEFAULT_INDEX: "https://example.invalid/simple", + UV_EXTRA_INDEX_URL: "https://example.invalid/simple", SHELLOPTS: "xtrace", PS4: "$(touch /tmp/pwned)", CLASSPATH: "/tmp/evil-classpath", @@ -267,6 +274,13 @@ describe("sanitizeHostExecEnv", () => { expect(env.GOFLAGS).toBeUndefined(); expect(env.PHPRC).toBeUndefined(); expect(env.XDG_CONFIG_HOME).toBeUndefined(); + expect(env.PIP_INDEX_URL).toBeUndefined(); + expect(env.PIP_PYPI_URL).toBeUndefined(); + expect(env.PIP_EXTRA_INDEX_URL).toBeUndefined(); + expect(env.UV_INDEX).toBeUndefined(); + expect(env.UV_INDEX_URL).toBeUndefined(); + expect(env.UV_DEFAULT_INDEX).toBeUndefined(); + expect(env.UV_EXTRA_INDEX_URL).toBeUndefined(); expect(env.SAFE).toBe("ok"); expect(env.HOME).toBe("/tmp/trusted-home"); expect(env.ZDOTDIR).toBe("/tmp/trusted-zdotdir"); @@ -354,6 +368,13 @@ describe("isDangerousHostEnvOverrideVarName", () => { expect(isDangerousHostEnvOverrideVarName("git_config_global")).toBe(true); expect(isDangerousHostEnvOverrideVarName("GRADLE_USER_HOME")).toBe(true); expect(isDangerousHostEnvOverrideVarName("gradle_user_home")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("PIP_INDEX_URL")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("pip_pypi_url")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("PIP_EXTRA_INDEX_URL")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("UV_INDEX")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("UV_INDEX_URL")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("uv_default_index")).toBe(true); + expect(isDangerousHostEnvOverrideVarName("UV_EXTRA_INDEX_URL")).toBe(true); expect(isDangerousHostEnvOverrideVarName("CLASSPATH")).toBe(true); expect(isDangerousHostEnvOverrideVarName("classpath")).toBe(true); expect(isDangerousHostEnvOverrideVarName("GOFLAGS")).toBe(true); @@ -380,6 +401,13 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { CXX: "/tmp/evil-cxx", CMAKE_C_COMPILER: "/tmp/evil-c-compiler", CLASSPATH: "/tmp/evil-classpath", + PIP_INDEX_URL: "https://example.invalid/simple", + PIP_PYPI_URL: "https://example.invalid/simple", + PIP_EXTRA_INDEX_URL: "https://example.invalid/simple", + UV_INDEX: "https://example.invalid/simple", + UV_INDEX_URL: "https://example.invalid/simple", + UV_DEFAULT_INDEX: "https://example.invalid/simple", + UV_EXTRA_INDEX_URL: "https://example.invalid/simple", SAFE_KEY: "ok", "BAD-KEY": "bad", }, @@ -390,6 +418,13 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { "CMAKE_C_COMPILER", "CXX", "PATH", + "PIP_EXTRA_INDEX_URL", + "PIP_INDEX_URL", + "PIP_PYPI_URL", + "UV_DEFAULT_INDEX", + "UV_EXTRA_INDEX_URL", + "UV_INDEX", + "UV_INDEX_URL", ]); expect(result.rejectedOverrideInvalidKeys).toEqual(["BAD-KEY"]); expect(result.env.SAFE_KEY).toBe("ok"); @@ -397,6 +432,13 @@ describe("sanitizeHostExecEnvWithDiagnostics", () => { expect(result.env.CLASSPATH).toBeUndefined(); expect(result.env.CXX).toBeUndefined(); expect(result.env.CMAKE_C_COMPILER).toBeUndefined(); + expect(result.env.PIP_INDEX_URL).toBeUndefined(); + expect(result.env.PIP_PYPI_URL).toBeUndefined(); + expect(result.env.PIP_EXTRA_INDEX_URL).toBeUndefined(); + expect(result.env.UV_INDEX).toBeUndefined(); + expect(result.env.UV_INDEX_URL).toBeUndefined(); + expect(result.env.UV_DEFAULT_INDEX).toBeUndefined(); + expect(result.env.UV_EXTRA_INDEX_URL).toBeUndefined(); }); it("allows Windows-style override names while still rejecting invalid keys", () => {