{"record":{"id":"16f4206d0c6cf9dc","repo":"garrytan/gstack","slug":"path-must-be-within-safedirs-join","errorCode":null,"errorMessage":"Path must be within: ${safeDirs.join(', ')}","messagePattern":"Path must be within: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/snapshot.ts","lineNumber":384,"sourceCode":"      });\n      let realPath: string;\n      try {\n        realPath = nodeFs.realpathSync(absolute);\n      } catch (err: any) {\n        if (err.code === 'ENOENT') {\n          try {\n            const dir = nodeFs.realpathSync(nodePath.dirname(absolute));\n            realPath = nodePath.join(dir, nodePath.basename(absolute));\n          } catch (err2: any) {\n            if (err2?.code !== 'ENOENT') throw err2;\n            realPath = absolute;\n          }\n        } else {\n          throw new Error(`Cannot resolve real path: ${screenshotPath} (${err.code})`);\n        }\n      }\n      if (!safeDirs.some((dir: string) => isPathWithin(realPath, dir))) {\n        throw new Error(`Path must be within: ${safeDirs.join(', ')}`);\n      }\n    }\n    try {\n      // Inject overlay divs at each ref's bounding box\n      const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number } }> = [];\n      for (const [ref, entry] of refMap) {\n        try {\n          const box = await entry.locator.boundingBox({ timeout: 1000 });\n          if (box) {\n            boxes.push({ ref: `@${ref}`, box });\n          }\n        } catch (err: any) {\n          // Element may be offscreen, hidden, or page navigated — skip\n          if (!err?.message?.includes('Timeout') && !err?.message?.includes('timeout') && !err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('Execution context')) throw err;\n        }\n      }\n\n      await page.evaluate((boxes) => {","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/snapshot.ts#L366-L402","documentation":"Thrown by the annotated-screenshot sandbox check in snapshot.ts. After resolving the realpath of the requested output path, the code asserts it lives inside one of the safe directories (TEMP_DIR or process.cwd()). If realpath escaped both — e.g. via a symlink or an absolute --output-path elsewhere on disk — the write is refused before page.screenshot() is ever called.","triggerScenarios":"Passing opts.outputPath (or letting it default to an absolute path) whose realpath resolves outside [TEMP_DIR, process.cwd()]; a symlink inside cwd that points to /etc or another tree; running the CLI from a working directory that itself symlinks elsewhere after realpathSync.","commonSituations":"Hard-coding an --output-path like '/tmp/other-tool/shot.png' where /tmp/other-tool is a symlink to /var/www; chaining snapshots across processes that share a cwd but write to different roots; container mounts where cwd resolves through a symlink to a host path.","solutions":["Point --output-path at a location physically inside the current working directory (verify with `readlink -f`).","Remove or rewrite symlinks in the output path that escape the safe dirs.","Launch the browse process from the directory you want outputs to land in so process.cwd() covers it.","Set TMPDIR to a writable temp area you control if you need the TEMP_DIR branch."],"exampleFix":"// before\nsnapshot(page, { annotate: true, outputPath: '/var/www/shot.png' }); // outside cwd & TEMP_DIR\n// after\nsnapshot(page, { annotate: true, outputPath: './shots/shot.png' }); // inside cwd","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst path = require('path');\nfunction assertPathWithin(p: string, safeDirs: string[]): void {\n  const real = fs.realpathSync(p); // throws if not resolvable\n  const ok = safeDirs.some(d => real === d || real.startsWith(d + path.sep));\n  if (!ok) throw new Error(`Path must be within: ${safeDirs.join(', ')}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await snapshot(session, { annotate: true, outputPath });\n} catch (e: any) {\n  if (/^Path must be within:/.test(e.message)) {\n    outputPath = path.join(process.cwd(), 'browse-annotated.png');\n    await snapshot(session, { annotate: true, outputPath });\n  } else throw e;\n}","preventionTips":["Resolve --output-path with path.resolve against process.cwd() so it always lands inside the sandbox.","Avoid symlinks in the output path or resolve them first with fs.realpathSync.","Document the safe-dir list for your team so callers know where outputs may live.","Treat the sandbox as a hard contract — do not attempt to widen it from caller code."],"tags":["security","path-traversal","sandbox","screenshot","symlink-escape"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}