{"record":{"id":"fedb315175f81eb6","repo":"microsoft/playwright","slug":"file-access-denied-resolvedfilename-is-outside","errorCode":null,"errorMessage":"File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}","messagePattern":"File access denied: (.+?) is outside allowed roots\\. Allowed roots: (.+?), (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/tools/backend/context.ts","lineNumber":423,"sourceCode":"\nexport async function outputFile(options: ContextOptions, fileName: string, flags: { origin: 'code' | 'llm' }): Promise<string> {\n  const resolvedFile = path.resolve(outputDir(options), fileName);\n  await checkFile(options, resolvedFile, flags);\n  await fs.promises.mkdir(path.dirname(resolvedFile), { recursive: true });\n  debug('pw:mcp:file')(resolvedFile);\n  return resolvedFile;\n}\n\nasync function checkFile(options: ContextOptions, resolvedFilename: string, flags: { origin: 'code' | 'llm' }) {\n  // Trust code and unrestricted file access.\n  if (flags.origin === 'code' || options.config.allowUnrestrictedFileAccess || options.config.skillMode)\n    return;\n\n  // Trust llm to use valid characters in file names.\n  const output = outputDir(options);\n  const workspace = options.cwd;\n  if (!isPathInside(output, resolvedFilename) && !isPathInside(workspace, resolvedFilename))\n    throw new Error(`File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}`);\n}\n","sourceCodeStart":405,"sourceCodeEnd":425,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/tools/backend/context.ts#L405-L425","documentation":"Thrown by checkFile (used by workspaceFile/outputFile) when an LLM-origin (flags.origin === 'llm') file path resolves outside both the output directory and the workspace (options.cwd). Code-origin calls, allowUnrestrictedFileAccess, and skillMode all bypass this sandbox check.","triggerScenarios":"An LLM-driven MCP tool resolving a file path (read, write, screenshot output, PDF output, upload) that resolves via path.resolve to a location outside outputDir(options) AND outside options.cwd.","commonSituations":"Agent passes an absolute path like /etc/passwd or /tmp/secret; relative path with ../ that escapes the workspace; outputDir falling back to a tmp dir while the agent assumed the workspace dir.","solutions":["Use a workspace-relative path (relative to options.cwd) for the file.","If writing tool artifacts, route through outputFile() which already anchors to outputDir.","For trusted automation, enable allowUnrestrictedFileAccess or skillMode in the config to bypass the sandbox.","Verify the resolved absolute path with path.resolve and confirm it lives under cwd before invoking the tool."],"exampleFix":"// before (agent supplies absolute path outside roots)\nawait workspaceFile(options, '/etc/secrets.txt'); // throws\n\n// after\nawait workspaceFile(options, 'artifacts/secrets.txt'); // resolves under options.cwd","handlingStrategy":"validation","validationCode":"import path from 'path';\n\nfunction isInside(root: string, candidate: string): boolean {\n  const rel = path.relative(root, candidate);\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nfunction assertFileAllowed(opts: { cwd: string; outputDir?: string }, absPath: string) {\n  const output = opts.outputDir ?? /* mirror outputDir() logic */ path.join(opts.cwd, '.playwright-mcp');\n  if (!isInside(output, absPath) && !isInside(opts.cwd, absPath))\n    throw new Error(`Refusing path outside roots: ${absPath}`);\n}","typeGuard":"function isPathInsideRoot(root: string, candidate: string): boolean {\n  const rel = path.relative(root, candidate);\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}","tryCatchPattern":"try {\n  await context.workspaceFile(name, perCallDir);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('File access denied:')) {\n    // rewrite to a workspace-relative path and retry\n  } else throw e;\n}","preventionTips":["Always resolve agent-supplied paths with path.resolve(workspace, name) before use.","Restrict LLM tool inputs to relative paths; reject absolute paths upstream.","If unrestricted access is genuinely needed, set allowUnrestrictedFileAccess deliberately and document the trust decision."],"tags":["security","filesystem","sandbox","llm-origin","config-gate"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}