diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 52a56cb2e4c2..8cbd1a511235 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -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. diff --git a/docs/help/environment.md b/docs/help/environment.md index 57cab7f14059..c5c23f64a43f 100644 --- a/docs/help/environment.md +++ b/docs/help/environment.md @@ -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. diff --git a/docs/help/faq.md b/docs/help/faq.md index 104ae7225f66..7f7d54a32199 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -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: diff --git a/docs/providers/azure-speech.md b/docs/providers/azure-speech.md index b2c15ed8ee72..4beee31e1946 100644 --- a/docs/providers/azure-speech.md +++ b/docs/providers/azure-speech.md @@ -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 diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index f8305263d763..05b57ccebe30 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -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"); }); }); diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 0aff739a8c89..5e79b3ab93a9 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -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__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_",