diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c29cff9a4..3912be92767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai - Gateway/restart: preserve one-shot continuation instructions across gateway restarts so agents can resume and reply back to the original chat after reboot. (#63406) Thanks @VACInc. - Gateway/restart: write restart sentinel files atomically so interrupted writes cannot leave a truncated sentinel behind. (#70225) Thanks @obviyus. - Pairing: remove stale pending requests for a device when that paired device is deleted, so an old repair approval cannot recreate the removed device from leftover state. +- Security/dotenv: block workspace `.env` overrides for Matrix, Mattermost, IRC, and Synology endpoint settings so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. (#70240) Thanks @drobison00. ## 2026.4.21 diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 9e5617009bb..9940cf56a42 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -624,6 +624,9 @@ describe("workspace .env blocklist completeness", () => { "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS", "OPENCLAW_BROWSER_EXECUTABLE_PATH", "EXAMPLE_API_HOST", + "IRC_HOST", + "MATTERMOST_URL", + "MATRIX_HOMESERVER", "MINIMAX_API_HOST", "BROWSER_EXECUTABLE_PATH", "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", @@ -643,6 +646,8 @@ describe("workspace .env blocklist completeness", () => { "OPENCLAW_NODE_EXEC_HOST", "OPENCLAW_NODE_EXEC_FALLBACK", "OPENCLAW_ALLOW_PROJECT_LOCAL_BIN", + "SYNOLOGY_CHAT_INCOMING_URL", + "SYNOLOGY_NAS_HOST", ]; await writeEnvFile( @@ -684,6 +689,62 @@ describe("workspace .env blocklist completeness", () => { }); }); + it("blocks bundled connector endpoint vars from workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + await writeEnvFile( + path.join(cwdDir, ".env"), + [ + "MATRIX_HOMESERVER=https://evil-matrix.example.com", + "MATTERMOST_URL=https://evil-mattermost.example.com", + "IRC_HOST=evil-irc.example.com", + "SYNOLOGY_CHAT_INCOMING_URL=https://evil-synology.example.com/incoming", + "SYNOLOGY_NAS_HOST=evil-synology.example.com", + "SAFE_PROVIDER_URL=https://allowed.example.com", + ].join("\n"), + ); + + delete process.env.MATRIX_HOMESERVER; + delete process.env.MATTERMOST_URL; + delete process.env.IRC_HOST; + delete process.env.SYNOLOGY_CHAT_INCOMING_URL; + delete process.env.SYNOLOGY_NAS_HOST; + delete process.env.SAFE_PROVIDER_URL; + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + expect(process.env.MATRIX_HOMESERVER).toBeUndefined(); + expect(process.env.MATTERMOST_URL).toBeUndefined(); + expect(process.env.IRC_HOST).toBeUndefined(); + expect(process.env.SYNOLOGY_CHAT_INCOMING_URL).toBeUndefined(); + expect(process.env.SYNOLOGY_NAS_HOST).toBeUndefined(); + expect(process.env.SAFE_PROVIDER_URL).toBe("https://allowed.example.com"); + }); + }); + }); + + it("blocks Matrix per-account scoped homeserver vars from workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + await writeEnvFile( + path.join(cwdDir, ".env"), + [ + "MATRIX_DEFAULT_HOMESERVER=https://evil-default.example.com", + "MATRIX_OPS_HOMESERVER=https://evil-ops.example.com", + ].join("\n"), + ); + + delete process.env.MATRIX_DEFAULT_HOMESERVER; + delete process.env.MATRIX_OPS_HOMESERVER; + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + expect(process.env.MATRIX_DEFAULT_HOMESERVER).toBeUndefined(); + expect(process.env.MATRIX_OPS_HOMESERVER).toBeUndefined(); + }); + }); + }); + it("blocks generic endpoint-routing suffixes from workspace .env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ cwdDir }) => { diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 391c30778af..631ae7cf113 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -21,6 +21,9 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "CLAWHUB_URL", "HTTP_PROXY", "HTTPS_PROXY", + "IRC_HOST", + "MATTERMOST_URL", + "MATRIX_HOMESERVER", "MINIMAX_API_HOST", "NODE_TLS_REJECT_UNAUTHORIZED", "NO_PROXY", @@ -66,11 +69,15 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "OPENCLAW_TEST_TAILSCALE_BINARY", "PI_CODING_AGENT_DIR", "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", + "SYNOLOGY_CHAT_INCOMING_URL", + "SYNOLOGY_NAS_HOST", "UV_PYTHON", ]); // Block endpoint redirection for any service without overfitting per-provider names. -const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_API_HOST", "_BASE_URL"]; +// `_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_PREFIXES = [ "ANTHROPIC_API_KEY_", "CLAWHUB_",