From e9ee6c5b4d50ccbd1426b3299f402bf84e13b6c6 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Wed, 10 Jun 2026 20:33:56 -0700 Subject: [PATCH] Harden Windows browser launcher --- skills/brainstorming/scripts/server.cjs | 33 +++++++--- .../browser-launcher.test.js | 66 +++++++++++++++++++ tests/brainstorm-server/package.json | 2 +- 3 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 tests/brainstorm-server/browser-launcher.test.js diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index f1a3b1ac..bce67e32 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -214,6 +214,20 @@ function companionUrl() { return 'http://' + urlHostForHttp(URL_HOST) + ':' + PORT + '/?key=' + TOKEN; } +function browserLauncherForPlatform(url, { + platform = process.platform, + osRelease = require('os').release(), + env = process.env +} = {}) { + const isWSL = platform === 'linux' && /microsoft/i.test(osRelease); + if (platform === 'darwin') return { bin: 'open', args: [url] }; + if (platform === 'win32' || isWSL) { + return { bin: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }; + } + if (env.DISPLAY || env.WAYLAND_DISPLAY) return { bin: 'xdg-open', args: [url] }; + return null; +} + function isRegularFileInsideContentDir(filePath) { let stat, realContentDir, realFilePath; try { @@ -455,13 +469,9 @@ function maybeOpenBrowser() { } // Platform launchers: pass the URL as an argv element via execFile (no shell), // so a url-host containing shell metacharacters can't inject a command. - const isWSL = process.platform === 'linux' && /microsoft/i.test(require('os').release()); - let bin, args; - if (process.platform === 'darwin') { bin = 'open'; args = [url]; } - else if (process.platform === 'win32' || isWSL) { bin = 'cmd.exe'; args = ['/c', 'start', '', url]; } - else if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) { bin = 'xdg-open'; args = [url]; } - else return; // headless: nothing to open - try { cp.execFile(bin, args, () => {}); } catch (e) { /* best effort */ } + const launcher = browserLauncherForPlatform(url); + if (!launcher) return; // headless: nothing to open + try { cp.execFile(launcher.bin, launcher.args, () => {}); } catch (e) { /* best effort */ } } // ========== Activity Tracking ========== @@ -627,4 +637,11 @@ if (require.main === module) { startServer(); } -module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES, MAX_FRAME_PAYLOAD_BYTES }; +module.exports = { + computeAcceptKey, + encodeFrame, + decodeFrame, + browserLauncherForPlatform, + OPCODES, + MAX_FRAME_PAYLOAD_BYTES +}; diff --git a/tests/brainstorm-server/browser-launcher.test.js b/tests/brainstorm-server/browser-launcher.test.js new file mode 100644 index 00000000..4bb9825c --- /dev/null +++ b/tests/brainstorm-server/browser-launcher.test.js @@ -0,0 +1,66 @@ +const assert = require('assert'); +const { + browserLauncherForPlatform +} = require('../../skills/brainstorming/scripts/server.cjs'); + +let passed = 0; +let failed = 0; + +async function test(name, fn) { + try { + await fn(); + console.log(` PASS: ${name}`); + passed++; + } catch (e) { + console.log(` FAIL: ${name}`); + console.log(` ${e.message}`); + failed++; + } +} + +(async () => { + console.log('\n--- Browser Launcher ---'); + + await test('Windows launcher does not route URLs through cmd.exe', () => { + const url = 'http://localhost:54122/?key=abc&x=SAFE&echo=INJECTED'; + const launcher = browserLauncherForPlatform(url, { + platform: 'win32', + osRelease: '10.0.26200', + env: {} + }); + + assert.deepStrictEqual(launcher, { + bin: 'rundll32.exe', + args: ['url.dll,FileProtocolHandler', url] + }); + assert(!launcher.args.includes('/c'), 'Windows launcher must not pass /c to a command interpreter'); + }); + + await test('WSL launcher does not route URLs through cmd.exe', () => { + const url = 'http://localhost:54122/?key=abc&x=SAFE&echo=INJECTED'; + const launcher = browserLauncherForPlatform(url, { + platform: 'linux', + osRelease: '5.15.167.4-microsoft-standard-WSL2', + env: {} + }); + + assert.deepStrictEqual(launcher, { + bin: 'rundll32.exe', + args: ['url.dll,FileProtocolHandler', url] + }); + }); + + await test('Linux launcher stays headless without a display', () => { + assert.strictEqual( + browserLauncherForPlatform('http://localhost:1/', { + platform: 'linux', + osRelease: '6.0.0', + env: {} + }), + null + ); + }); + + console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`); + if (failed > 0) process.exit(1); +})(); diff --git a/tests/brainstorm-server/package.json b/tests/brainstorm-server/package.json index dc0c5216..978f81c5 100644 --- a/tests/brainstorm-server/package.json +++ b/tests/brainstorm-server/package.json @@ -2,7 +2,7 @@ "name": "brainstorm-server-tests", "version": "1.0.0", "scripts": { - "test": "node ws-protocol.test.js && node helper.test.js && node auth.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh" + "test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh" }, "dependencies": { "ws": "^8.19.0"