From d631326c5e785c0223a837e19a92f689ca1a9b25 Mon Sep 17 00:00:00 2001 From: Agustin Rivera <31522568+eleqtrizit@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:39:10 -0700 Subject: [PATCH] fix(tailscale): gate test binary override (#58468) * fix(tailscale): gate test binary override * fix(changelog): note tailscale override hardening * fix(changelog): drop tailscale note from pr * chore: add changelog for tailscale test binary gating --------- Co-authored-by: Devin Robison --- CHANGELOG.md | 1 + src/infra/dotenv.test.ts | 17 +++++++++++++++++ src/infra/dotenv.ts | 1 + src/infra/tailscale.test.ts | 20 +++++++++++++++++++- src/infra/tailscale.ts | 13 ++++++++++++- 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 535329d2f9d..bdb5e4da307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Infra/tailscale: ignore `OPENCLAW_TEST_TAILSCALE_BINARY` outside explicit test environments and block it from workspace `.env`, so test-only binary overrides cannot be injected through trusted repository state. (#58468) Thanks @eleqtrizit. - Plugins/OpenAI: enable reference-image edits for `gpt-image-1` by routing edit calls to `/images/edits` with multipart image uploads, and update image-generation capability/docs metadata accordingly. - Agents/tools: include value-shape hints in missing-parameter tool errors so dropped, empty-string, and wrong-type write payloads are easier to diagnose from logs. (#55317) Thanks @priyansh19. - Android/assistant: keep queued App Actions prompts pending when auto-send enqueue is rejected, so transient chat-health drops do not silently lose the assistant request. Thanks @obviyus. diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 5e8b1da20c4..132fd137e55 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -236,6 +236,23 @@ describe("loadDotEnv", () => { }); }); + it("blocks OPENCLAW_TEST_TAILSCALE_BINARY from workspace .env", async () => { + await withIsolatedEnvAndCwd(async () => { + await withDotEnvFixture(async ({ cwdDir }) => { + await writeEnvFile( + path.join(cwdDir, ".env"), + "OPENCLAW_TEST_TAILSCALE_BINARY=/tmp/attacker-tailscale\n", + ); + + delete process.env.OPENCLAW_TEST_TAILSCALE_BINARY; + + loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); + + expect(process.env.OPENCLAW_TEST_TAILSCALE_BINARY).toBeUndefined(); + }); + }); + }); + it("blocks pinned helper interpreter vars from workspace .env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ cwdDir }) => { diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 10c2e38e345..1abe3136c75 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -34,6 +34,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "OPENCLAW_PINNED_WRITE_PYTHON", "OPENCLAW_PROFILE", "OPENCLAW_STATE_DIR", + "OPENCLAW_TEST_TAILSCALE_BINARY", "OPENAI_API_KEY", "OPENAI_API_KEYS", "PI_CODING_AGENT_DIR", diff --git a/src/infra/tailscale.test.ts b/src/infra/tailscale.test.ts index 37658c2b287..09839a6e881 100644 --- a/src/infra/tailscale.test.ts +++ b/src/infra/tailscale.test.ts @@ -6,6 +6,7 @@ const { ensureGoInstalled, ensureTailscaledInstalled, getTailnetHostname, + getTestTailscaleBinaryOverride, enableTailscaleServe, disableTailscaleServe, ensureFunnel, @@ -33,8 +34,9 @@ describe("tailscale helpers", () => { let envSnapshot: ReturnType; beforeEach(() => { - envSnapshot = captureEnv(["OPENCLAW_TEST_TAILSCALE_BINARY"]); + envSnapshot = captureEnv(["OPENCLAW_TEST_TAILSCALE_BINARY", "NODE_ENV", "VITEST"]); process.env.OPENCLAW_TEST_TAILSCALE_BINARY = "tailscale"; + process.env.VITEST ??= "true"; }); afterEach(() => { @@ -69,6 +71,22 @@ describe("tailscale helpers", () => { expect(host).toBe("noisy.tailnet.ts.net"); }); + it("allows the test binary override in explicit test environments", () => { + process.env.OPENCLAW_TEST_TAILSCALE_BINARY = "/tmp/test-tailscale"; + process.env.NODE_ENV = "test"; + delete process.env.VITEST; + + expect(getTestTailscaleBinaryOverride()).toBe("/tmp/test-tailscale"); + }); + + it("ignores the test binary override outside test environments", () => { + process.env.OPENCLAW_TEST_TAILSCALE_BINARY = "/tmp/attacker-tailscale"; + process.env.NODE_ENV = "production"; + delete process.env.VITEST; + + expect(getTestTailscaleBinaryOverride()).toBeNull(); + }); + it.each([ { name: "ensureGoInstalled installs when missing and user agrees", diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index c2244b19b98..4f688698f63 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -149,8 +149,19 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte */ let cachedTailscaleBinary: string | null = null; +export function getTestTailscaleBinaryOverride(env: NodeJS.ProcessEnv = process.env): string | null { + const forcedBinary = env.OPENCLAW_TEST_TAILSCALE_BINARY?.trim(); + if (!forcedBinary) { + return null; + } + if (env.VITEST || env.NODE_ENV === "test") { + return forcedBinary; + } + return null; +} + export async function getTailscaleBinary(): Promise { - const forcedBinary = process.env.OPENCLAW_TEST_TAILSCALE_BINARY?.trim(); + const forcedBinary = getTestTailscaleBinaryOverride(); if (forcedBinary) { cachedTailscaleBinary = forcedBinary; return forcedBinary;