diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8870056c1..76fbbe2ef83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ Docs: https://docs.openclaw.ai - Agents/failover: carry `sessionId`, `lane`, `provider`, `model`, and `profileId` attribution through `FailoverError` and `describeFailoverError`/`coerceToFailoverError` so structured error logs (e.g. `gateway.err.log` ingestion) can attribute exhausted-fallback wrapper errors to the originating session and last-attempted provider instead of dropping the metadata after the per-profile errors. Fixes #42713. (#73506) Thanks @wenxu007. - Context Engine: treat assembled prompt as the default authority for preemptive overflow prechecks so engines that return a windowed, self-contained context no longer trigger false hard-fail compactions on huge raw history. Engines whose assembled view can hide overflow risk can opt back into the legacy behavior with `AssembleResult.promptAuthority: "preassembly_may_overflow"`. (#74255) Thanks @100yenadmin. - Mattermost: refresh current native slash command registrations before accepting callbacks so stale tokens from deleted or regenerated commands stop being accepted without a gateway restart while failed validations stay briefly cached and lookup starts are rate-limited per command, gate each callback against the resolved command's own startup token so a token leaked for one slash command cannot poison another command's failure cache, redact slash validation lookup errors, and add a body read timeout to the multi-account routing path so slow callback senders cannot tie up the dispatcher. Thanks @feynman-hou and @eleqtrizit. +- Security/dotenv: block `COMSPEC` in workspace `.env` so a malicious repo cannot redirect Windows `cmd.exe` resolution, and lock in case-insensitive workspace-`.env` regression coverage for the full Windows shell trust-root family (`COMSPEC`, `PROGRAMFILES`, `PROGRAMW6432`, `SYSTEMROOT`, `WINDIR`). (#74460) Thanks @mmaps. ## 2026.4.29 diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 6f0907404ac..95b2fd574a6 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -41,6 +41,19 @@ const BUNDLED_TRUST_ROOT_ENV_KEYS = BUNDLED_TRUST_ROOT_ENV_LINES.map( (line) => line.split("=")[0] ?? "", ); +const WINDOWS_SHELL_TRUST_ROOT_ENV_KEYS = [ + "ComSpec", + "COMSPEC", + "ProgramFiles", + "PROGRAMFILES", + "ProgramW6432", + "PROGRAMW6432", + "SystemRoot", + "SYSTEMROOT", + "windir", + "WINDIR", +] as const; + async function writeEnvFile(filePath: string, contents: string) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, contents, "utf8"); @@ -305,6 +318,34 @@ describe("loadDotEnv", () => { }); }); + it("blocks Windows shell trust-root vars from workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + await writeEnvFile( + path.join(cwdDir, ".env"), + [ + "ComSpec=.\\evil-comspec", + "COMSPEC=.\\evil-comspec-upper", + "ProgramFiles=.\\evil-pfiles", + "PROGRAMFILES=.\\evil-pfiles-upper", + "ProgramW6432=.\\evil-pw6432", + "PROGRAMW6432=.\\evil-pw6432-upper", + "SystemRoot=.\\fake-root", + "SYSTEMROOT=.\\fake-root-upper", + "windir=.\\fake-windir", + "WINDIR=.\\fake-windir-upper", + ].join("\n"), + ); + + clearEnv(WINDOWS_SHELL_TRUST_ROOT_ENV_KEYS); + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + expectEnvUndefined(WINDOWS_SHELL_TRUST_ROOT_ENV_KEYS); + }); + }); + }); + it("blocks path-override vars (OPENCLAW_AGENT_DIR, OPENCLAW_BUNDLED_PLUGINS_DIR, PI_CODING_AGENT_DIR, OPENCLAW_OAUTH_DIR) from workspace .env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 34e7eb977b9..47ae1c96bd1 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -23,6 +23,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "CLAWHUB_TOKEN", "CLAWHUB_URL", "CLOUDSDK_PYTHON", + "COMSPEC", "HTTP_PROXY", "HTTPS_PROXY", "HOMEBREW_BREW_FILE",