fix: block workspace dotenv endpoint overrides [AI] (#103914)

* fix: block workspace dotenv endpoint overrides

* docs: clarify workspace endpoint env sources

* style: format dotenv suffix blocklist

---------

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
This commit is contained in:
Michael Appel
2026-07-14 12:51:15 +05:30
committed by GitHub
co-authored by Pavan Kumar Gondhi
parent ced9a4ae22
commit 18ec9ce8f7
6 changed files with 15 additions and 7 deletions
+1 -1
View File
@@ -758,7 +758,7 @@ OpenClaw loads workspace-local `.env` files for agents and tools, but never lets
- Provider credential environment variables are blocked from untrusted workspace `.env` files - for example `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY`, `EXA_API_KEY`, `FIRECRAWL_API_KEY`, and provider auth keys declared by installed trusted plugins. Put provider credentials in the Gateway process environment, `~/.openclaw/.env` (`$OPENCLAW_STATE_DIR/.env`), the config `env` block, or an optional login-shell import instead.
- Any key starting with `OPENCLAW_` is blocked from untrusted workspace `.env` files, reserving the whole runtime namespace so a future `OPENCLAW_*` control is fail-closed by default rather than silently inheritable from checked-in or attacker-supplied `.env` content.
- Channel endpoint settings for Matrix, Mattermost, IRC, and Synology Chat are also blocked from workspace `.env` overrides (for example `MATRIX_HOMESERVER`, `MATTERMOST_URL`, `IRC_HOST`, `SYNOLOGY_CHAT_INCOMING_URL`), so a cloned workspace cannot redirect bundled connector traffic through local endpoint config. These must come from the gateway process environment or `env.shellEnv`.
- Channel and provider endpoint-routing settings are also blocked from workspace `.env` overrides (for example `MATRIX_HOMESERVER`, `MATTERMOST_URL`, `IRC_HOST`, `SYNOLOGY_CHAT_INCOMING_URL`, `AZURE_SPEECH_ENDPOINT`, and other keys ending in `_ENDPOINT`), so a cloned workspace cannot redirect bundled connector traffic through local endpoint config. These must come from the gateway process environment, global runtime dotenv, explicit config, or `env.shellEnv`.
- Trusted process/OS environment variables, global runtime dotenv, config `env`, and enabled login-shell import still apply - this only constrains workspace `.env` file loading.
Workspace `.env` files frequently live next to agent code, get committed by accident, or get written by tools; blocking provider credentials prevents a cloned workspace from substituting attacker-controlled provider accounts.
+2 -2
View File
@@ -24,7 +24,7 @@ If the config file is missing entirely, step 4 is skipped; shell import still ru
## Provider credentials and workspace `.env`
Do not keep provider API keys only in a workspace `.env`. OpenClaw blocks a large set of provider credential and endpoint-redirect keys from workspace `.env` files, including every known provider auth env var (for example `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY`, `EXA_API_KEY`, `FIRECRAWL_API_KEY`), plus any key ending in `_API_HOST`, `_BASE_URL`, or `_HOMESERVER`, and the entire `OPENCLAW_*`, `CLAWHUB_*`, `ANTHROPIC_API_KEY_*`, and `OPENAI_API_KEY_*` namespaces.
Do not keep provider API keys only in a workspace `.env`. OpenClaw blocks a large set of provider credential and endpoint-redirect keys from workspace `.env` files, including every known provider auth env var (for example `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY`, `EXA_API_KEY`, `FIRECRAWL_API_KEY`), plus any key ending in `_API_HOST`, `_BASE_URL`, `_ENDPOINT`, or `_HOMESERVER`, and the entire `OPENCLAW_*`, `CLAWHUB_*`, `ANTHROPIC_API_KEY_*`, and `OPENAI_API_KEY_*` namespaces.
Use one of these trusted sources for provider credentials instead:
@@ -33,7 +33,7 @@ Use one of these trusted sources for provider credentials instead:
- The config `env` block in `~/.openclaw/openclaw.json`.
- Optional login-shell import when `env.shellEnv.enabled` or `OPENCLAW_LOAD_SHELL_ENV=1` is enabled.
If you previously stored provider keys only in a workspace `.env`, move them to one of the trusted sources above. Workspace `.env` can still provide ordinary project variables that are not credentials, endpoint redirects, host overrides, or `OPENCLAW_*` runtime controls.
If you previously stored provider keys or endpoint routing values only in a workspace `.env`, move them to one of the trusted sources above. Workspace `.env` can still provide ordinary project variables that are not credentials, endpoint redirects, host overrides, or `OPENCLAW_*` runtime controls.
See [Workspace `.env` files](/gateway/security#workspace-env-files) for the security rationale.
+1 -1
View File
@@ -900,7 +900,7 @@ First-run Q&A - install, onboard, auth routes, subscriptions, initial failures -
- `.env` from the current working directory.
- a global fallback `.env` from `~/.openclaw/.env` (`$OPENCLAW_STATE_DIR/.env`).
Neither `.env` file overrides existing env vars. Provider credential keys are an exception for workspace `.env`: keys such as `GEMINI_API_KEY`, `XAI_API_KEY`, or `MISTRAL_API_KEY` (and other bundled-provider auth env vars) are ignored from workspace `.env` and should live in the process environment, `~/.openclaw/.env`, or config `env`.
Neither `.env` file overrides existing env vars. Provider credential and endpoint-routing keys are an exception for workspace `.env`: keys such as `GEMINI_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, or any key ending in `_ENDPOINT` (and other bundled-provider auth or endpoint env vars) are ignored from workspace `.env` and should live in the process environment, `~/.openclaw/.env`, or config `env`.
Inline env vars in config apply only if missing from the process env:
+4 -2
View File
@@ -69,7 +69,7 @@ All options live under `messages.tts.providers["azure-speech"]`.
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| `apiKey` | Azure Speech resource key. Falls back to `AZURE_SPEECH_KEY`, `AZURE_SPEECH_API_KEY`, or `SPEECH_KEY`. |
| `region` | Azure Speech resource region. Falls back to `AZURE_SPEECH_REGION` or `SPEECH_REGION`. |
| `endpoint` | Optional Azure Speech endpoint override. Falls back to `AZURE_SPEECH_ENDPOINT`. |
| `endpoint` | Optional Azure Speech endpoint override. Falls back to trusted `AZURE_SPEECH_ENDPOINT`. |
| `baseUrl` | Optional Azure Speech base URL override. |
| `voice` | Azure voice ShortName (default `en-US-JennyNeural`). Legacy alias: `voiceId`. |
| `lang` | SSML language code (default `en-US`). |
@@ -79,7 +79,9 @@ All options live under `messages.tts.providers["azure-speech"]`.
The provider is considered configured once `apiKey` is set plus one of
`region`, `endpoint`, or `baseUrl`. Env vars are only checked as a fallback
for config keys left unset.
for config keys left unset. Workspace `.env` files cannot set
`AZURE_SPEECH_ENDPOINT`; use the process environment, global runtime dotenv,
or explicit config for endpoint routing.
## Notes
+6
View File
@@ -1065,6 +1065,7 @@ describe("workspace .env blocklist completeness", () => {
"IRC_HOST=evil-irc.example.com",
"SYNOLOGY_CHAT_INCOMING_URL=https://evil-synology.example.com/incoming",
"SYNOLOGY_NAS_HOST=evil-synology.example.com",
"AZURE_SPEECH_ENDPOINT=https://evil-speech.example.com",
"SAFE_PROVIDER_URL=https://allowed.example.com",
].join("\n"),
);
@@ -1074,6 +1075,7 @@ describe("workspace .env blocklist completeness", () => {
delete process.env.IRC_HOST;
delete process.env.SYNOLOGY_CHAT_INCOMING_URL;
delete process.env.SYNOLOGY_NAS_HOST;
delete process.env.AZURE_SPEECH_ENDPOINT;
delete process.env.SAFE_PROVIDER_URL;
loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true });
@@ -1083,6 +1085,7 @@ describe("workspace .env blocklist completeness", () => {
expect(process.env.IRC_HOST).toBeUndefined();
expect(process.env.SYNOLOGY_CHAT_INCOMING_URL).toBeUndefined();
expect(process.env.SYNOLOGY_NAS_HOST).toBeUndefined();
expect(process.env.AZURE_SPEECH_ENDPOINT).toBeUndefined();
expect(process.env.SAFE_PROVIDER_URL).toBe("https://allowed.example.com");
});
});
@@ -1118,18 +1121,21 @@ describe("workspace .env blocklist completeness", () => {
[
"FUTURE_PROVIDER_API_HOST=https://evil.example.com",
"FUTURE_PROVIDER_BASE_URL=https://evil.example.com/v1",
"FUTURE_PROVIDER_ENDPOINT=https://evil.example.com/api",
"SAFE_PROVIDER_URL=https://allowed.example.com",
].join("\n"),
);
delete process.env.FUTURE_PROVIDER_API_HOST;
delete process.env.FUTURE_PROVIDER_BASE_URL;
delete process.env.FUTURE_PROVIDER_ENDPOINT;
delete process.env.SAFE_PROVIDER_URL;
loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true });
expect(process.env.FUTURE_PROVIDER_API_HOST).toBeUndefined();
expect(process.env.FUTURE_PROVIDER_BASE_URL).toBeUndefined();
expect(process.env.FUTURE_PROVIDER_ENDPOINT).toBeUndefined();
expect(process.env.SAFE_PROVIDER_URL).toBe("https://allowed.example.com");
});
});
+1 -1
View File
@@ -179,7 +179,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
// Block endpoint redirection for any service without overfitting per-provider names.
// `_HOMESERVER` covers Matrix's per-account scoped keys (MATRIX_<ACCOUNT>_HOMESERVER)
// in addition to the bare MATRIX_HOMESERVER listed above.
const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_API_HOST", "_BASE_URL", "_HOMESERVER"];
const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_API_HOST", "_BASE_URL", "_ENDPOINT", "_HOMESERVER"];
const BLOCKED_WORKSPACE_DOTENV_PREFIXES = [
"ANTHROPIC_API_KEY_",
"CLAWHUB_",