fix(dotenv): block workspace runtime env vars (#62660)

* fix(dotenv): block workspace runtime env vars

Co-authored-by: zsx <git@zsxsoft.com>

* docs(changelog): add workspace dotenv runtime-control entry

* fix(dotenv): block workspace gateway port override

---------

Co-authored-by: zsx <git@zsxsoft.com>
Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
Agustin Rivera
2026-04-08 12:04:00 -07:00
committed by GitHub
parent f4704184f6
commit dbfcef3196
3 changed files with 113 additions and 3 deletions

View File

@@ -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 (`&lt;`/`&gt;`) in media-tag regexes so entity-escaped `<qqimg>` 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.

View File

@@ -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");
});
});
});
});

View File

@@ -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);