From 30a6493ac3c2f3703961803e4f13bb85597b82e9 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Wed, 26 Aug 2026 10:33:13 -0600 Subject: [PATCH] fix(server): filter artifact paths in unsafe mode remote servers started with `--unsafe` can use filesystem paths that only exist on the client allow client `artifactsDir` and `downloadsPath` only in extension mode --- .../src/remote/playwrightServer.ts | 9 +++-- tests/config/remoteServer.ts | 4 +- tests/library/browsertype-connect.spec.ts | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index c1928ce1e6b5e..56f5103c19a28 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -109,7 +109,7 @@ export class PlaywrightServer { } const isExtension = this._options.mode === 'extension'; - launchOptions = filterLaunchOptions(launchOptions, isExtension || !!this._options.unsafe); + launchOptions = filterLaunchOptions(launchOptions, isExtension, !!this._options.unsafe); // Always override artifacts dir with the one from server options. if (this._options.artifactsDir) @@ -365,7 +365,8 @@ function launchOptionsHash(options: LaunchOptionsWithTimeout) { return JSON.stringify(copy); } -function filterLaunchOptions(options: LaunchOptionsWithTimeout, allowUnsafe: boolean): LaunchOptionsWithTimeout { +function filterLaunchOptions(options: LaunchOptionsWithTimeout, isExtension: boolean, unsafe: boolean): LaunchOptionsWithTimeout { + const allowUnsafe = isExtension || unsafe; return { channel: options.channel, args: allowUnsafe ? options.args : undefined, @@ -378,8 +379,8 @@ function filterLaunchOptions(options: LaunchOptionsWithTimeout, allowUnsafe: boo firefoxUserPrefs: (isUnderTest() || allowUnsafe) ? options.firefoxUserPrefs : undefined, slowMo: options.slowMo, executablePath: (isUnderTest() || allowUnsafe) ? options.executablePath : undefined, - downloadsPath: allowUnsafe ? options.downloadsPath : undefined, - artifactsDir: (isUnderTest() || allowUnsafe) ? options.artifactsDir : undefined, + downloadsPath: (isUnderTest() || isExtension) ? options.downloadsPath : undefined, + artifactsDir: (isUnderTest() || isExtension) ? options.artifactsDir : undefined, }; } diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index fc334af94d9d2..da753d5091972 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -27,12 +27,14 @@ export class RunServer implements PlaywrightServer { private _process!: TestChildProcess; _wsEndpoint!: string; - async start(childProcess: CommonFixtures['childProcess'], options?: { mode?: 'extension' | 'default', env?: NodeJS.ProcessEnv, artifactsDir?: string }) { + async start(childProcess: CommonFixtures['childProcess'], options?: { mode?: 'extension' | 'default', env?: NodeJS.ProcessEnv, artifactsDir?: string, unsafe?: boolean }) { const command = ['node', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'cli.js'), 'run-server']; if (options?.mode === 'extension') command.push('--mode=extension'); if (options?.artifactsDir) command.push(`--artifacts-dir=${options.artifactsDir}`); + if (options?.unsafe) + command.push('--unsafe'); this._process = childProcess({ command, env: { NODE_OPTIONS: process.env.NODE_OPTIONS, ...options?.env }, diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 15e4da5883d10..e134a5c8f456c 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -1162,6 +1162,43 @@ test('should refuse connecting when versions do not match', async ({ connect, ch expect(error.message).toContain('client version: v' + getPlaywrightVersion(true)); }); +test('should filter local paths from unsafe launch options', async ({ connect, childProcess, server }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42394' }); + server.setRoute('/download', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment'); + res.end('Hello world'); + }); + const artifactsDir = testInfo.outputPath('artifacts'); + const downloadsPath = testInfo.outputPath('downloads'); + fs.writeFileSync(artifactsDir, 'not a directory'); + fs.writeFileSync(downloadsPath, 'not a directory'); + const remoteServer = new RunServer(); + await remoteServer.start(childProcess, { unsafe: true, env: { PWTEST_UNDER_TEST: undefined } }); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { + 'x-playwright-launch-options': JSON.stringify({ artifactsDir, downloadsPath }), + }, + }); + const context = await browser.newContext(); + await context.tracing.start({ snapshots: true }); + const page = await context.newPage(); + await page.setContent(`download`); + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('a'), + ]); + expect(await download.failure()).toBeNull(); + const tracePath = testInfo.outputPath('trace.zip'); + await context.tracing.stop({ path: tracePath }); + await context.close(); + await browser.close(); + await remoteServer.close(); + + const { actions } = await parseTraceRaw(tracePath); + expect(actions).toContain('Set content'); +}); + test('should timeout after redirect when connecting over http', async ({ connect, server }) => { server.setRedirect('/connect/json', '/connect/slow'); let aborted = false;