diff --git a/CHANGELOG.md b/CHANGELOG.md index e23faa46dad..dd851a65a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Slack/media: preserve bearer auth across same-origin `files.slack.com` redirects while still stripping it on cross-origin Slack CDN hops, so `url_private_download` image attachments load again. (#62960) Thanks @vincentkoc. - Gateway/node exec events: mark remote node `exec.started`, `exec.finished`, and `exec.denied` summaries as untrusted system events and sanitize node-provided command/output/reason text before enqueueing them, so remote node output cannot inject trusted `System:` content into later turns. (#62659) Thanks @eleqtrizit. - Agents/timeouts: make the LLM idle timeout inherit `agents.defaults.timeoutSeconds` when configured, disable the unconfigured idle watchdog for cron runs, and point idle-timeout errors at `agents.defaults.llm.idleTimeoutSeconds`. Thanks @drvoss. +- Security/dotenv: expand workspace `.env` filtering to block runtime-control variables like gateway routing, ClawHub endpoints/tokens, browser executable overrides, and skip/disable control families, so untrusted repositories cannot steer OpenClaw runtime behavior through repo-local dotenv files. (#62660) Thanks @eleqtrizit. - Agents/failover: classify Z.ai vendor code `1311` as billing and `1113` as auth, including long wrapped `1311` payloads, so these errors stop falling through to generic failover handling. (#49552) Thanks @1bcMax. - QQBot/media-tags: support HTML entity-encoded angle brackets (`<`/`>`) in media-tag regexes so entity-escaped `` tags from upstream are correctly parsed and normalized. (#60493) Thanks @ylc0919. - npm packaging: mirror bundled Slack, Telegram, Discord, and Feishu channel runtime deps at the root and harden published-install verification so fresh installs fail fast on manifest drift instead of missing-module crashes. (#63065) Thanks @scoootscooob. diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index ea82f227ad2..8c89ddbd464 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -613,3 +613,80 @@ describe("loadCliDotEnv", () => { }); }); }); + +describe("workspace .env blocklist completeness", () => { + it("blocks runtime-control variables from workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + const runtimeControlKeys = [ + "OPENCLAW_UPDATE_PACKAGE_SPEC", + "OPENCLAW_GATEWAY_PORT", + "OPENCLAW_GATEWAY_URL", + "OPENCLAW_CLAWHUB_URL", + "CLAWHUB_URL", + "OPENCLAW_CLAWHUB_TOKEN", + "CLAWHUB_TOKEN", + "CLAWHUB_AUTH_TOKEN", + "CLAWHUB_CONFIG_PATH", + "OPENCLAW_DISABLE_BUNDLED_PLUGINS", + "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS", + "OPENCLAW_BROWSER_EXECUTABLE_PATH", + "BROWSER_EXECUTABLE_PATH", + "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", + "OPENCLAW_SKIP_CHANNELS", + "OPENCLAW_SKIP_PROVIDERS", + "OPENCLAW_SKIP_CRON", + "OPENCLAW_RAW_STREAM", + "OPENCLAW_RAW_STREAM_PATH", + "OPENCLAW_CACHE_TRACE", + "OPENCLAW_CACHE_TRACE_FILE", + "OPENCLAW_CACHE_TRACE_MESSAGES", + "OPENCLAW_CACHE_TRACE_PROMPT", + "OPENCLAW_CACHE_TRACE_SYSTEM", + "OPENCLAW_SHOW_SECRETS", + "OPENCLAW_PLUGIN_CATALOG_PATHS", + "OPENCLAW_MPM_CATALOG_PATHS", + "OPENCLAW_NODE_EXEC_HOST", + "OPENCLAW_NODE_EXEC_FALLBACK", + "OPENCLAW_ALLOW_PROJECT_LOCAL_BIN", + ]; + + await writeEnvFile( + path.join(cwdDir, ".env"), + `${runtimeControlKeys.map((key) => `${key}=INJECTED_${key}`).join("\n")}\n`, + ); + + for (const key of runtimeControlKeys) { + delete process.env[key]; + } + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + for (const key of runtimeControlKeys) { + expect(process.env[key], `${key} should be blocked by workspace .env`).toBeUndefined(); + } + }); + }); + }); + + it("still allows user-defined non-control vars through workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + await writeEnvFile( + path.join(cwdDir, ".env"), + "MY_APP_KEY=user-value\nGITHUB_TOKEN=ghp_test123\nDATABASE_URL_CUSTOM=pg://localhost\n", + ); + + delete process.env.MY_APP_KEY; + delete process.env.GITHUB_TOKEN; + delete process.env.DATABASE_URL_CUSTOM; + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + expect(process.env.MY_APP_KEY).toBe("user-value"); + expect(process.env.GITHUB_TOKEN).toBe("ghp_test123"); + expect(process.env.DATABASE_URL_CUSTOM).toBe("pg://localhost"); + }); + }); + }); +}); diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 1c3589db8c3..ed2259e9a15 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -14,37 +14,69 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "ALL_PROXY", "ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", + "BROWSER_EXECUTABLE_PATH", + "CLAWHUB_AUTH_TOKEN", + "CLAWHUB_CONFIG_PATH", + "CLAWHUB_TOKEN", + "CLAWHUB_URL", "HTTP_PROXY", "HTTPS_PROXY", "NODE_TLS_REJECT_UNAUTHORIZED", "NO_PROXY", + "OPENAI_API_KEY", + "OPENAI_API_KEYS", "OPENCLAW_AGENT_DIR", + "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS", + "OPENCLAW_ALLOW_PROJECT_LOCAL_BIN", + "OPENCLAW_BROWSER_EXECUTABLE_PATH", "OPENCLAW_BUNDLED_HOOKS_DIR", "OPENCLAW_BUNDLED_PLUGINS_DIR", "OPENCLAW_BUNDLED_SKILLS_DIR", + "OPENCLAW_CACHE_TRACE", + "OPENCLAW_CACHE_TRACE_FILE", + "OPENCLAW_CACHE_TRACE_MESSAGES", + "OPENCLAW_CACHE_TRACE_PROMPT", + "OPENCLAW_CACHE_TRACE_SYSTEM", "OPENCLAW_CONFIG_PATH", "OPENCLAW_GATEWAY_PASSWORD", + "OPENCLAW_GATEWAY_PORT", "OPENCLAW_GATEWAY_SECRET", "OPENCLAW_GATEWAY_TOKEN", + "OPENCLAW_GATEWAY_URL", "OPENCLAW_HOME", "OPENCLAW_LIVE_ANTHROPIC_KEY", "OPENCLAW_LIVE_ANTHROPIC_KEYS", "OPENCLAW_LIVE_GEMINI_KEY", "OPENCLAW_LIVE_OPENAI_KEY", + "OPENCLAW_MPM_CATALOG_PATHS", + "OPENCLAW_NODE_EXEC_FALLBACK", + "OPENCLAW_NODE_EXEC_HOST", "OPENCLAW_OAUTH_DIR", "OPENCLAW_PINNED_PYTHON", "OPENCLAW_PINNED_WRITE_PYTHON", + "OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_PROFILE", + "OPENCLAW_RAW_STREAM", + "OPENCLAW_RAW_STREAM_PATH", + "OPENCLAW_SHOW_SECRETS", "OPENCLAW_STATE_DIR", "OPENCLAW_TEST_TAILSCALE_BINARY", - "OPENAI_API_KEY", - "OPENAI_API_KEYS", "PI_CODING_AGENT_DIR", + "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "UV_PYTHON", ]); +// Block endpoint redirection for any service without overfitting per-provider names. const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_BASE_URL"]; -const BLOCKED_WORKSPACE_DOTENV_PREFIXES = ["ANTHROPIC_API_KEY_", "OPENAI_API_KEY_"]; +const BLOCKED_WORKSPACE_DOTENV_PREFIXES = [ + "ANTHROPIC_API_KEY_", + "CLAWHUB_", + "OPENAI_API_KEY_", + "OPENCLAW_CLAWHUB_", + "OPENCLAW_DISABLE_", + "OPENCLAW_SKIP_", + "OPENCLAW_UPDATE_", +]; function shouldBlockWorkspaceRuntimeDotEnvKey(key: string): boolean { return isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key);