From 61858b914f2ac82d6055e139635b07af08ac370f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 17 Mar 2026 18:28:24 -0600 Subject: [PATCH] fix(security): use execFile for browser URL opening to prevent shell injection (#1022) Co-authored-by: Claude Opus 4.6 (1M context) --- .../modes/interactive/components/login-dialog.ts | 8 ++++---- src/onboarding.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts index 790c37737..20b62bc0c 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts @@ -2,7 +2,7 @@ // Copyright (c) 2026 Jeremy McSpadden import { getOAuthProviders } from "@gsd/pi-ai/oauth"; import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@gsd/pi-tui"; -import { exec } from "child_process"; +import { execFile } from "child_process"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; import { keyHint } from "./keybinding-hints.js"; @@ -134,11 +134,11 @@ export class LoginDialogComponent extends Container implements Focusable { // Try to open browser — on Windows, `start` needs an empty title arg // so it treats the URL as a target, not a window title - const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; if (process.platform === "win32") { - exec(`start "" "${url}"`); + execFile("cmd", ["/c", "start", "", url], () => {}); } else { - exec(`${openCmd} "${url}"`); + const openCmd = process.platform === "darwin" ? "open" : "xdg-open"; + execFile(openCmd, [url], () => {}); } this.tui.requestRender(); diff --git a/src/onboarding.ts b/src/onboarding.ts index 2d858c0d8..ed75d2c6c 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -10,7 +10,7 @@ * All steps are skippable. All errors are recoverable. Never crashes boot. */ -import { exec } from 'node:child_process' +import { execFile } from 'node:child_process' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import type { AuthStorage } from '@gsd/pi-coding-agent' @@ -122,12 +122,12 @@ async function loadPico(): Promise { /** Open a URL in the system browser (best-effort, non-blocking) */ function openBrowser(url: string): void { - const cmd = process.platform === 'darwin' ? 'open' : - process.platform === 'win32' ? 'start' : - 'xdg-open' - exec(`${cmd} "${url}"`, () => { - // Ignore errors — user can manually open the URL - }) + if (process.platform === 'win32') { + execFile('cmd', ['/c', 'start', '', url], () => {}) + } else { + const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open' + execFile(cmd, [url], () => {}) + } } /** Check if an error is a clack cancel signal */