microsoft/playwright · error · Error

Attachment name '${fileName}' escapes output directory

Error message

Attachment name '${fileName}' escapes output directory

What it means

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.

Source

Thrown at packages/playwright-core/src/tools/trace/traceUtils.ts:119

  const totalMs = Math.floor(relative);
  const minutes = Math.floor(totalMs / 60000);
  const seconds = Math.floor((totalMs % 60000) / 1000);
  const millis = totalMs % 1000;
  return `${minutes}:${seconds.toString().padStart(2, '0')}.${millis.toString().padStart(3, '0')}`;
}

export function actionTitle(action: ActionEntry): string {
  return renderTitleForCall({ ...action, type: action.class }) || `${action.class}.${action.method}`;
}

export async function saveOutputFile(fileName: string, content: string | Buffer, explicitOutput?: string): Promise<string> {
  let outFile: string;
  if (explicitOutput) {
    outFile = explicitOutput;
  } else {
    const resolved = resolveWithinRoot(cliOutputDir, fileName);
    if (!resolved)
      throw new Error(`Attachment name '${fileName}' escapes output directory`);
    await fs.promises.mkdir(path.dirname(resolved), { recursive: true });
    outFile = resolved;
  }
  await fs.promises.writeFile(outFile, content);
  return outFile;
}


function buildOrdinalMap(model: TraceModel): { ordinalToCallId: Map<number, string>, callIdToOrdinal: Map<string, number> } {
  const actions = model.actions.filter(a => a.group !== 'configuration');
  const { rootItem } = buildActionTree(actions);
  const ordinalToCallId = new Map<number, string>();
  const callIdToOrdinal = new Map<string, number>();
  let ordinal = 1;
  const visit = (item: ReturnType<typeof buildActionTree>['rootItem']) => {
    ordinalToCallId.set(ordinal, item.action.callId);
    callIdToOrdinal.set(item.action.callId, ordinal);
    ordinal++;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Pass an explicit output path via the explicitOutput parameter to write to a known location.
  2. Sanitize the attachment file name: strip path separators and '..' segments before calling saveOutputFile.
  3. Investigate where the unsafe file name originated — it usually indicates a bug or untrusted input upstream.

Example fix

// before
saveOutputFile('../out/log.txt', content);
// after
saveOutputFile('log.txt', content);
Defensive patterns

Strategy: validation

Validate before calling

import path from 'path';
function isSafeAttachmentName(name: string): boolean {
  if (path.isAbsolute(name)) return false;
  const resolved = path.resolve('.playwright-cli', name);
  const root = path.resolve('.playwright-cli');
  return resolved === root || resolved.startsWith(root + path.sep);
}
if (!isSafeAttachmentName(fileName)) throw new Error(`Unsafe attachment name: ${fileName}`);

Type guard

function isSafeAttachmentName(name: string): boolean {
  return !path.isAbsolute(name) && !name.includes('..') && !name.includes(path.sep);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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