{"record":{"id":"eae989ffcb6f6cc8","repo":"microsoft/playwright","slug":"attachment-name-filename-escapes-output-direc","errorCode":null,"errorMessage":"Attachment name '${fileName}' escapes output directory","messagePattern":"Attachment name '(.+?)' escapes output directory","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/tools/trace/traceUtils.ts","lineNumber":119,"sourceCode":"  const totalMs = Math.floor(relative);\n  const minutes = Math.floor(totalMs / 60000);\n  const seconds = Math.floor((totalMs % 60000) / 1000);\n  const millis = totalMs % 1000;\n  return `${minutes}:${seconds.toString().padStart(2, '0')}.${millis.toString().padStart(3, '0')}`;\n}\n\nexport function actionTitle(action: ActionEntry): string {\n  return renderTitleForCall({ ...action, type: action.class }) || `${action.class}.${action.method}`;\n}\n\nexport async function saveOutputFile(fileName: string, content: string | Buffer, explicitOutput?: string): Promise<string> {\n  let outFile: string;\n  if (explicitOutput) {\n    outFile = explicitOutput;\n  } else {\n    const resolved = resolveWithinRoot(cliOutputDir, fileName);\n    if (!resolved)\n      throw new Error(`Attachment name '${fileName}' escapes output directory`);\n    await fs.promises.mkdir(path.dirname(resolved), { recursive: true });\n    outFile = resolved;\n  }\n  await fs.promises.writeFile(outFile, content);\n  return outFile;\n}\n\n\nfunction buildOrdinalMap(model: TraceModel): { ordinalToCallId: Map<number, string>, callIdToOrdinal: Map<string, number> } {\n  const actions = model.actions.filter(a => a.group !== 'configuration');\n  const { rootItem } = buildActionTree(actions);\n  const ordinalToCallId = new Map<number, string>();\n  const callIdToOrdinal = new Map<string, number>();\n  let ordinal = 1;\n  const visit = (item: ReturnType<typeof buildActionTree>['rootItem']) => {\n    ordinalToCallId.set(ordinal, item.action.callId);\n    callIdToOrdinal.set(item.action.callId, ordinal);\n    ordinal++;","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/tools/trace/traceUtils.ts#L101-L137","documentation":"Thrown by saveOutputFile() when the requested attachment file name, resolved against cliOutputDir (.playwright-cli), would escape that directory (resolveWithinRoot returns null). This is a path-traversal guard preventing a trace-derived file name containing '../' or an absolute path from writing outside the designated output directory. Only applies when no explicit output path was given; an explicit output bypasses the check.","triggerScenarios":"A tool/snapshot producing an attachment whose file name contains traversal sequences (e.g. '../evil.txt') or an absolute path, and the caller did not pass an explicit output path.","commonSituations":"Malformed or adversarial input feeding an attachment name; a bug producing a file name with leading slashes; normal Playwright output names should never trigger this.","solutions":["Pass an explicit output path via the explicitOutput parameter to write to a known location.","Sanitize the attachment file name: strip path separators and '..' segments before calling saveOutputFile.","Investigate where the unsafe file name originated — it usually indicates a bug or untrusted input upstream."],"exampleFix":"// before\nsaveOutputFile('../out/log.txt', content);\n// after\nsaveOutputFile('log.txt', content);","handlingStrategy":"validation","validationCode":"import path from 'path';\nfunction isSafeAttachmentName(name: string): boolean {\n  if (path.isAbsolute(name)) return false;\n  const resolved = path.resolve('.playwright-cli', name);\n  const root = path.resolve('.playwright-cli');\n  return resolved === root || resolved.startsWith(root + path.sep);\n}\nif (!isSafeAttachmentName(fileName)) throw new Error(`Unsafe attachment name: ${fileName}`);","typeGuard":"function isSafeAttachmentName(name: string): boolean {\n  return !path.isAbsolute(name) && !name.includes('..') && !name.includes(path.sep);\n}","tryCatchPattern":null,"preventionTips":["Pass plain file names (no path separators, no '..') as attachment names.","Provide an explicit output path when you need to write outside .playwright-cli.","Sanitize any file name derived from untrusted or external input."],"tags":["trace","security","path-traversal","filesystem","validation"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}