{"record":{"id":"ee6f9da4f6595794","repo":"vitest-dev/vitest","slug":"benchmark-artifact-path-relativepath-resolves","errorCode":null,"errorMessage":"Benchmark artifact path \"${relativePath}\" resolves outside the project root (${root}). Paths passed to `writeResult` and `bench.from()` must point inside the project.","messagePattern":"Benchmark artifact path \"(.+?)\" resolves outside the project root \\((.+?)\\)\\. Paths passed to `writeResult` and `bench\\.from\\(\\)` must point inside the project\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/node/benchmark.ts","lineNumber":21,"sourceCode":"import { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, resolve } from 'pathe'\n\nexport class BenchmarkManager {\n  constructor(private project: TestProject) {}\n\n  // Resolve a user-supplied path against the project root. Reject paths that\n  // escape the project root: `bench.from()` accepts arbitrary input, and we\n  // never want a benchmark file to be able to read or clobber files outside\n  // the workspace.\n  public resolve(relativePath: string): string {\n    const root = this.project.config.root\n    const absolute = isAbsolute(relativePath)\n      ? resolve(relativePath)\n      : resolve(root, relativePath)\n    const rootWithSep = root.endsWith('/') ? root : `${root}/`\n    if (absolute !== root && !absolute.startsWith(rootWithSep)) {\n      throw new Error(\n        `Benchmark artifact path \"${relativePath}\" resolves outside the project root (${root}). `\n        + `Paths passed to \\`writeResult\\` and \\`bench.from()\\` must point inside the project.`,\n      )\n    }\n    return absolute\n  }\n\n  async readResult(relativePath: string): Promise<BaselineData | null> {\n    const path = this.resolve(relativePath)\n    if (!existsSync(path)) {\n      return null\n    }\n    return JSON.parse(await readFile(path, 'utf-8')) as BaselineData\n  }\n\n  async writeResult(relativePath: string, data: BaselineData): Promise<void> {\n    const absolute = this.resolve(relativePath)\n    await mkdir(dirname(absolute), { recursive: true })","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/vitest-dev/vitest/blob/1fa9837ec26533512fdcad8baebf249771bd340a/packages/vitest/src/node/benchmark.ts#L3-L39","documentation":"A path-traversal guard inside `BenchmarkManager.resolve()`. Before reading or writing a benchmark baseline artifact via `bench.from()`/`writeResult`, the manager resolves the user-supplied path against the project `root` and rejects any result that does not equal or sit under `root/`. This prevents a benchmark file from reading or clobbering files outside the workspace.","triggerScenarios":"Calling `bench.from('../../../etc/passwd')`, `bench.from('/tmp/baseline.json')` (absolute path outside root), `bench.from('..\\..\\secrets\\key')` on Windows, or passing a symlink-laden path that `pathe/resolve` normalizes outside the project root. Affects both `readResult` and `writeResult` paths.","commonSituations":"Monorepo where the benchmark file is in `packages/a/` but `root` was resolved to a parent workspace and the relative path escapes; CI where `root` is `/repo` but baseline stored in a shared `/baselines` volume; accidentally passing an absolute cache path to `bench.from()`.","solutions":["Keep baseline artifacts inside the project tree, e.g. `bench.from('./.bench/baseline.json')`.","If you need a shared baseline location, set Vitest `root` (or `bench` output dir) to that location so the path resolves inside it.","Replace backslashes and `..` segments; verify with `path.resolve(root, relative).startsWith(root + path.sep)` before calling.","For symlinks, resolve real paths and ensure the target is within root before passing to `bench.from()`."],"exampleFix":"// before — escapes project root\nbench.from('/shared/baselines/core.json')\n\n// after — keep inside the project\nbench.from('./baselines/core.json')\n// or set root to the shared dir when starting Vitest: vitest --root /shared/baselines","handlingStrategy":"validation","validationCode":"import { isAbsolute, resolve, relative } from 'node:path'\n\nfunction assertWithinRoot(root: string, p: string): string {\n  const abs = isAbsolute(p) ? resolve(p) : resolve(root, p)\n  const rel = relative(root, abs)\n  if (rel.startsWith('..') || isAbsolute(rel)) {\n    throw new Error(`Refusing to read path outside root: ${p}`)\n  }\n  return abs\n}\n\nconst safe = assertWithinRoot(project.config.root, baselinePath)\nbench.from(safe)","typeGuard":"function isWithinRoot(root: string, p: string): boolean {\n  const abs = resolve(root, p)\n  const rootWithSep = root.endsWith('/') ? root : root + '/'\n  return abs === root || abs.startsWith(rootWithSep)\n}","tryCatchPattern":null,"preventionTips":["Never accept absolute paths from untrusted input for benchmark baselines.","Keep baseline files under a dedicated in-project directory like `./.bench/`.","Sanitize `..` segments before passing paths to `bench.from()`."],"tags":["security","path-traversal","benchmark","bench","validation"],"backgroundTag":null,"analyzedSha":"1fa9837ec26533512fdcad8baebf249771bd340a","analyzedAt":"2026-08-11T16:11:39.638Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}