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" + ); +});