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

  1. Treat this as the guard doing its job: the server is sending an unsafe filename; fix or sanitize the server-suggested filename.
  2. If you control the response, ensure Content-Disposition filenames are basenames only (no slashes, no '..').
  3. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/9e4bf5263f8115bc. Report an issue: GitHub.