fix(dotenv): block helper interpreter workspace overrides (#58473)

* fix(dotenv): block helper interpreter workspace overrides

* fix(dotenv): cover trusted helper interpreter envs

* fix(changelog): note dotenv helper override hardening

* fix(changelog): remove dotenv entry from pr

* changelog: note dotenv helper override hardening

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
This commit is contained in:
Agustin Rivera
2026-04-02 06:45:13 -07:00
committed by GitHub
parent 52a6e354a8
commit 290e5bf219
3 changed files with 35 additions and 1 deletions

View File

@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
- Gateway: prune empty `node-pending-work` state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.
- Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared `safeEqualSecret` helper and reject empty auth tokens in BlueBubbles. Thanks @eleqtrizit.
- OpenShell/mirror sync: constrain mirror sync to managed roots only so user-added shell roots are no longer overwritten or removed during config synchronization. (#58515) Thanks @eleqtrizit.
- Dotenv/workspace overrides: block workspace `.env` files from overriding `OPENCLAW_PINNED_PYTHON` and `OPENCLAW_PINNED_WRITE_PYTHON` so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.
## 2026.4.1-beta.1

View File

@@ -236,6 +236,28 @@ describe("loadDotEnv", () => {
});
});
it("blocks pinned helper interpreter vars from workspace .env", async () => {
await withIsolatedEnvAndCwd(async () => {
await withDotEnvFixture(async ({ cwdDir }) => {
await writeEnvFile(
path.join(cwdDir, ".env"),
[
"OPENCLAW_PINNED_PYTHON=./attacker-python",
"OPENCLAW_PINNED_WRITE_PYTHON=./attacker-write-python",
].join("\n"),
);
delete process.env.OPENCLAW_PINNED_PYTHON;
delete process.env.OPENCLAW_PINNED_WRITE_PYTHON;
loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true });
expect(process.env.OPENCLAW_PINNED_PYTHON).toBeUndefined();
expect(process.env.OPENCLAW_PINNED_WRITE_PYTHON).toBeUndefined();
});
});
});
it("blocks bundled trust-root vars from workspace .env", async () => {
await withIsolatedEnvAndCwd(async () => {
await withDotEnvFixture(async ({ cwdDir }) => {
@@ -266,16 +288,25 @@ describe("loadDotEnv", () => {
await withDotEnvFixture(async ({ cwdDir, stateDir }) => {
await writeEnvFile(
path.join(stateDir, ".env"),
"ANTHROPIC_BASE_URL=https://trusted.example.com/v1\nHTTP_PROXY=http://proxy.test:8080\n",
[
"ANTHROPIC_BASE_URL=https://trusted.example.com/v1",
"HTTP_PROXY=http://proxy.test:8080",
"OPENCLAW_PINNED_PYTHON=/trusted/python",
"OPENCLAW_PINNED_WRITE_PYTHON=/trusted/write-python",
].join("\n"),
);
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
delete process.env.ANTHROPIC_BASE_URL;
delete process.env.HTTP_PROXY;
delete process.env.OPENCLAW_PINNED_PYTHON;
delete process.env.OPENCLAW_PINNED_WRITE_PYTHON;
loadDotEnv({ quiet: true });
expect(process.env.ANTHROPIC_BASE_URL).toBe("https://trusted.example.com/v1");
expect(process.env.HTTP_PROXY).toBe("http://proxy.test:8080");
expect(process.env.OPENCLAW_PINNED_PYTHON).toBe("/trusted/python");
expect(process.env.OPENCLAW_PINNED_WRITE_PYTHON).toBe("/trusted/write-python");
});
});
});

View File

@@ -30,6 +30,8 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
"OPENCLAW_LIVE_GEMINI_KEY",
"OPENCLAW_LIVE_OPENAI_KEY",
"OPENCLAW_OAUTH_DIR",
"OPENCLAW_PINNED_PYTHON",
"OPENCLAW_PINNED_WRITE_PYTHON",
"OPENCLAW_PROFILE",
"OPENCLAW_STATE_DIR",
"OPENAI_API_KEY",