microsoft/playwright · error · Error
Download filename '${downloadFilename}' escapes download dir
Error message
Download filename '${downloadFilename}' escapes download directory What it means
Download paths are confined to the configured downloads directory via resolveWithinRoot. If the suggested download filename (from the server's Content-Disposition or the page) contains path traversal (../) or is absolute, resolveWithinRoot returns null and the Download constructor throws. This is a security guard preventing writes outside the downloads sandbox.
Source
Thrown at packages/playwright-core/src/server/download.ts:33
*/
import { assert } from '@isomorphic/assert';
import { resolveWithinRoot } from '@utils/fileUtils';
import { Page } from './page';
import { Artifact } from './artifact';
export class Download {
readonly artifact: Artifact;
readonly url: string;
private _uuid: string;
private _page: Page;
private _suggestedFilename: string | undefined;
constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string, downloadFilename?: string) {
const unaccessibleErrorMessage = page.browserContext._options.acceptDownloads === 'deny' ? 'Pass { acceptDownloads: true } when you are creating your browser context.' : undefined;
const downloadPath = resolveWithinRoot(downloadsPath, downloadFilename ?? uuid);
if (!downloadPath)
throw new Error(`Download filename '${downloadFilename}' escapes download directory`);
this.artifact = new Artifact(page, downloadPath, unaccessibleErrorMessage, () => this.cancel());
this._page = page;
this.url = url;
this._uuid = uuid;
this._suggestedFilename = suggestedFilename;
// Note: downloads are never removed from the context, so that we can delete them upon context closure.
page.browserContext._downloads.add(this);
if (suggestedFilename !== undefined)
this._fireDownloadEvent();
}
cancel() {
return this._page.browserContext.cancelDownload(this._uuid);
}
filenameSuggested(suggestedFilename: string) {
assert(this._suggestedFilename === undefined);
this._suggestedFilename = suggestedFilename;View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Treat this as the guard doing its job: the server is sending an unsafe filename; fix or sanitize the server-suggested filename.
- If you control the response, ensure Content-Disposition filenames are basenames only (no slashes, no '..').
- Report/ignore the download rather than acting on a traversal attempt.
Defensive patterns
Strategy: validation
Validate before calling
// Sanitize a server-suggested filename to a safe basename before accepting the download.
function safeFilename(name: string): string {
return path.basename(name).replace(/[^\w.\-]/g, '_') || 'download';
} Prevention
- Treat this error as the security guard working correctly — the source is unsafe.
- Sanitize Content-Disposition filenames to basenames in your own test server.
- Do not disable the downloads-path confinement.
When it happens
Trigger: A server sends Content-Disposition: attachment; filename="../../etc/passwd" or filename="/etc/cron.d/x", and the browser context accepts downloads; the download filename resolves outside the downloads root.
Common situations: Testing against a misconfigured or adversarial server whose headers contain traversal; extremely rare in normal automation since the guard exists precisely to block it.
Related errors
- HAR entry _file escapes base directory: ${file}
- HAR zip entry '${entry}' escapes output directory
- Trace entry '${entry}' escapes output directory
- Attachment name '${fileName}' escapes output directory
- ERROR: Playwright does not support ${descriptor.name} on ${h
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/9e4bf5263f8115bc.
Report an issue: GitHub.