{"record":{"id":"9e4bf5263f8115bc","repo":"microsoft/playwright","slug":"download-filename-downloadfilename-escapes-do","errorCode":null,"errorMessage":"Download filename '${downloadFilename}' escapes download directory","messagePattern":"Download filename '(.+?)' escapes download directory","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/server/download.ts","lineNumber":33,"sourceCode":" */\n\nimport { assert } from '@isomorphic/assert';\nimport { resolveWithinRoot } from '@utils/fileUtils';\nimport { Page } from './page';\nimport { Artifact } from './artifact';\n\nexport class Download {\n  readonly artifact: Artifact;\n  readonly url: string;\n  private _uuid: string;\n  private _page: Page;\n  private _suggestedFilename: string | undefined;\n\n  constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string, downloadFilename?: string) {\n    const unaccessibleErrorMessage = page.browserContext._options.acceptDownloads === 'deny' ? 'Pass { acceptDownloads: true } when you are creating your browser context.' : undefined;\n    const downloadPath = resolveWithinRoot(downloadsPath, downloadFilename ?? uuid);\n    if (!downloadPath)\n      throw new Error(`Download filename '${downloadFilename}' escapes download directory`);\n    this.artifact = new Artifact(page, downloadPath, unaccessibleErrorMessage, () => this.cancel());\n    this._page = page;\n    this.url = url;\n    this._uuid = uuid;\n    this._suggestedFilename = suggestedFilename;\n    // Note: downloads are never removed from the context, so that we can delete them upon context closure.\n    page.browserContext._downloads.add(this);\n    if (suggestedFilename !== undefined)\n      this._fireDownloadEvent();\n  }\n\n  cancel() {\n    return this._page.browserContext.cancelDownload(this._uuid);\n  }\n\n  filenameSuggested(suggestedFilename: string) {\n    assert(this._suggestedFilename === undefined);\n    this._suggestedFilename = suggestedFilename;","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/server/download.ts#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"Testing against a misconfigured or adversarial server whose headers contain traversal; extremely rare in normal automation since the guard exists precisely to block it.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Sanitize a server-suggested filename to a safe basename before accepting the download.\nfunction safeFilename(name: string): string {\n  return path.basename(name).replace(/[^\\w.\\-]/g, '_') || 'download';\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["security","download","path-traversal"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}