From 8d93f5a7cadf53fd5a2a560390514ec76eefb41e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 30 Apr 2026 19:35:48 -0700 Subject: [PATCH] fix(trace cli): confine trace attachment reads to trace dir Prevents path traversal via forged attachment.sha1 metadata and zip-slip during trace extraction. --- .../src/tools/trace/traceParser.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/tools/trace/traceParser.ts b/packages/playwright-core/src/tools/trace/traceParser.ts index a0804b0913106..9bf9487856523 100644 --- a/packages/playwright-core/src/tools/trace/traceParser.ts +++ b/packages/playwright-core/src/tools/trace/traceParser.ts @@ -16,6 +16,7 @@ import fs from 'fs'; import path from 'path'; +import { resolveWithinRoot } from '@utils/fileUtils'; import { ZipFile } from '@utils/zipFile'; import type { TraceLoaderBackend } from '@isomorphic/trace/traceLoader'; @@ -47,8 +48,11 @@ export class DirTraceLoaderBackend implements TraceLoaderBackend { } async hasEntry(entryName: string): Promise { + const resolved = resolveWithinRoot(this._dir, entryName); + if (!resolved) + return false; try { - await fs.promises.access(path.join(this._dir, entryName)); + await fs.promises.access(resolved); return true; } catch { return false; @@ -56,15 +60,21 @@ export class DirTraceLoaderBackend implements TraceLoaderBackend { } async readText(entryName: string): Promise { + const resolved = resolveWithinRoot(this._dir, entryName); + if (!resolved) + return; try { - return await fs.promises.readFile(path.join(this._dir, entryName), 'utf-8'); + return await fs.promises.readFile(resolved, 'utf-8'); } catch { } } async readBlob(entryName: string): Promise { + const resolved = resolveWithinRoot(this._dir, entryName); + if (!resolved) + return; try { - const buffer = await fs.promises.readFile(path.join(this._dir, entryName)); + const buffer = await fs.promises.readFile(resolved); return new Blob([new Uint8Array(buffer)]); } catch { } @@ -73,12 +83,17 @@ export class DirTraceLoaderBackend implements TraceLoaderBackend { export async function extractTrace(traceFile: string, outDir: string): Promise { const zipFile = new ZipFile(traceFile); - const entries = await zipFile.entries(); - for (const entry of entries) { - const outPath = path.join(outDir, entry); - await fs.promises.mkdir(path.dirname(outPath), { recursive: true }); - const buffer = await zipFile.read(entry); - await fs.promises.writeFile(outPath, buffer); + try { + const entries = await zipFile.entries(); + for (const entry of entries) { + const outPath = resolveWithinRoot(outDir, entry); + if (!outPath) + throw new Error(`Trace entry '${entry}' escapes output directory`); + await fs.promises.mkdir(path.dirname(outPath), { recursive: true }); + const buffer = await zipFile.read(entry); + await fs.promises.writeFile(outPath, buffer); + } + } finally { + zipFile.close(); } - zipFile.close(); }