diff --git a/src/lib/cloudflaredTunnel.ts b/src/lib/cloudflaredTunnel.ts index e5d1f41b86..d706b7ed4e 100644 --- a/src/lib/cloudflaredTunnel.ts +++ b/src/lib/cloudflaredTunnel.ts @@ -3,6 +3,7 @@ import { promisify } from "util"; import fs from "fs/promises"; import fsSync from "fs"; import path from "path"; +import proxyFetch from "@omniroute/open-sse/utils/proxyFetch.ts"; import { resolveDataDir } from "@/lib/dataPaths"; import { getRuntimePorts } from "@/lib/runtime/ports"; @@ -58,6 +59,47 @@ export type CloudflaredTunnelStatus = { logPath: string; }; +const CLOUDFLARED_SAFE_ENV_KEYS = [ + "PATH", + "HOME", + "USERPROFILE", + "APPDATA", + "LOCALAPPDATA", + "PROGRAMDATA", + "ProgramData", + "SYSTEMROOT", + "SystemRoot", + "WINDIR", + "ComSpec", + "COMSPEC", + "PATHEXT", + "TMPDIR", + "TMP", + "TEMP", + "USER", + "USERNAME", + "LOGNAME", + "SHELL", + "LANG", + "LC_ALL", + "LC_CTYPE", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "NODE_EXTRA_CA_CERTS", + "XDG_CONFIG_HOME", + "XDG_CACHE_HOME", + "XDG_DATA_HOME", + "XDG_RUNTIME_DIR", + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "no_proxy", +] as const; + let tunnelProcess: ReturnType | null = null; let tunnelPid: number | null = null; let installPromise: Promise | null = null; @@ -159,6 +201,21 @@ export function extractTryCloudflareUrl(text: string) { return match ? match[0] : null; } +export function buildCloudflaredChildEnv( + sourceEnv: NodeJS.ProcessEnv = process.env +): NodeJS.ProcessEnv { + const childEnv: NodeJS.ProcessEnv = {}; + + for (const key of CLOUDFLARED_SAFE_ENV_KEYS) { + const value = sourceEnv[key]; + if (typeof value === "string" && value.length > 0) { + childEnv[key] = value; + } + } + + return childEnv; +} + export function getCloudflaredAssetSpec( platform = process.platform, arch = process.arch @@ -251,7 +308,7 @@ async function extractArchive(archivePath: string, destinationDir: string) { } async function downloadToFile(url: string, destinationPath: string) { - const response = await fetch(url, { redirect: "follow" }); + const response = await proxyFetch(url, { redirect: "follow" }); if (!response.ok) { throw new Error(`Download failed with status ${response.status}`); } @@ -455,7 +512,7 @@ export async function startCloudflaredTunnel(): Promise ["tunnel", "--url", targetUrl, "--no-autoupdate", "--protocol", "http2"], { stdio: ["ignore", "pipe", "pipe"], - env: process.env, + env: buildCloudflaredChildEnv(), } ); diff --git a/tests/unit/cloudflaredTunnel.test.mjs b/tests/unit/cloudflaredTunnel.test.mjs index 005ee7c7e0..dcde778ccd 100644 --- a/tests/unit/cloudflaredTunnel.test.mjs +++ b/tests/unit/cloudflaredTunnel.test.mjs @@ -2,6 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { + buildCloudflaredChildEnv, extractTryCloudflareUrl, getCloudflaredAssetSpec, } from "../../src/lib/cloudflaredTunnel.ts"; @@ -45,3 +46,19 @@ test("getCloudflaredAssetSpec resolves darwin arm64 archive", () => { test("getCloudflaredAssetSpec returns null for unsupported platforms", () => { assert.equal(getCloudflaredAssetSpec("freebsd", "x64"), null); }); + +test("buildCloudflaredChildEnv keeps runtime essentials but drops secrets", () => { + const env = buildCloudflaredChildEnv({ + PATH: "/usr/bin", + HOME: "/tmp/home", + HTTPS_PROXY: "http://proxy.internal:8080", + JWT_SECRET: "top-secret", + API_KEY_SECRET: "another-secret", + }); + + assert.deepEqual(env, { + PATH: "/usr/bin", + HOME: "/tmp/home", + HTTPS_PROXY: "http://proxy.internal:8080", + }); +});