From 436ce4869fe25f063df5a397db3a1b5876bd84f4 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:12:38 +0200 Subject: [PATCH] fix(tailscale): validate ports and quote MSI paths (#11534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined 10-PR batch worktree off release/v3.8.51 tip. - Focused test: tests/unit/tailscale-validation.test.ts — 2/2 pass plus 13/13 related Tailscale tests - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK - Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip — unrelated to this diff ⚠️ base-red inherited: #11449 Thanks for rejecting out-of-range Tailscale ports and escaping apostrophes before they reach the Windows MSI installer PowerShell command. --- .../fixes/tailscale-port-and-windows-path.md | 1 + src/app/api/tunnels/tailscale/routeUtils.ts | 2 +- src/lib/tailscaleTunnel.ts | 12 ++++++------ tests/unit/tailscale-validation.test.ts | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/tailscale-port-and-windows-path.md create mode 100644 tests/unit/tailscale-validation.test.ts diff --git a/changelog.d/fixes/tailscale-port-and-windows-path.md b/changelog.d/fixes/tailscale-port-and-windows-path.md new file mode 100644 index 0000000000..0becdaa9c3 --- /dev/null +++ b/changelog.d/fixes/tailscale-port-and-windows-path.md @@ -0,0 +1 @@ +- Fixed Tailscale tunnel setup to reject ports outside the valid 1-65535 range and safely install Windows MSI packages from paths containing apostrophes. diff --git a/src/app/api/tunnels/tailscale/routeUtils.ts b/src/app/api/tunnels/tailscale/routeUtils.ts index 131005bcbb..b6df5118e5 100644 --- a/src/app/api/tunnels/tailscale/routeUtils.ts +++ b/src/app/api/tunnels/tailscale/routeUtils.ts @@ -6,7 +6,7 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export const tailscaleEnableSchema = z.object({ sudoPassword: z.string().optional(), hostname: z.string().optional(), - port: z.number().int().positive().optional(), + port: z.number().int().min(1).max(65535).optional(), }); export const tailscaleLoginSchema = z.object({ diff --git a/src/lib/tailscaleTunnel.ts b/src/lib/tailscaleTunnel.ts index e98e2f0fc6..1db4d9b1ca 100644 --- a/src/lib/tailscaleTunnel.ts +++ b/src/lib/tailscaleTunnel.ts @@ -1104,6 +1104,11 @@ async function installTailscaleLinux(password: string, onProgress?: (message: st }); } +export function buildWindowsTailscaleInstallCommand(msiPath: string): string { + const escapedMsiPath = msiPath.replace(/'/g, "''"); + return `Start-Process msiexec -ArgumentList '/i','${escapedMsiPath}','TS_NOLAUNCH=true','/quiet','/norestart' -Verb RunAs -Wait`; +} + async function installTailscaleWindows(onProgress?: (message: string) => void) { const msiUrl = "https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi"; const msiPath = path.join(os.tmpdir(), "tailscale-setup.msi"); @@ -1125,12 +1130,7 @@ async function installTailscaleWindows(onProgress?: (message: string) => void) { await new Promise((resolve, reject) => { const child = spawn( "powershell", - [ - "-NoProfile", - "-NonInteractive", - "-Command", - `Start-Process msiexec -ArgumentList '/i','${msiPath}','TS_NOLAUNCH=true','/quiet','/norestart' -Verb RunAs -Wait`, - ], + ["-NoProfile", "-NonInteractive", "-Command", buildWindowsTailscaleInstallCommand(msiPath)], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"], diff --git a/tests/unit/tailscale-validation.test.ts b/tests/unit/tailscale-validation.test.ts new file mode 100644 index 0000000000..baed8ef6ae --- /dev/null +++ b/tests/unit/tailscale-validation.test.ts @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { tailscaleEnableSchema } from "../../src/app/api/tunnels/tailscale/routeUtils.ts"; +import { buildWindowsTailscaleInstallCommand } from "../../src/lib/tailscaleTunnel.ts"; + +test("tailscale enable accepts only valid TCP ports", () => { + assert.equal(tailscaleEnableSchema.safeParse({ port: 1 }).success, true); + assert.equal(tailscaleEnableSchema.safeParse({ port: 65535 }).success, true); + assert.equal(tailscaleEnableSchema.safeParse({ port: 0 }).success, false); + assert.equal(tailscaleEnableSchema.safeParse({ port: 65536 }).success, false); +}); + +test("Windows installer command escapes apostrophes in MSI paths", () => { + assert.equal( + buildWindowsTailscaleInstallCommand("C:\\Users\\O'Brien\\tailscale-setup.msi"), + "Start-Process msiexec -ArgumentList '/i','C:\\Users\\O''Brien\\tailscale-setup.msi','TS_NOLAUNCH=true','/quiet','/norestart' -Verb RunAs -Wait" + ); +});