{"record":{"id":"86282fb588bcc8c0","repo":"nexu-io/open-design","slug":"project-files-read-json-path-escapes-project-dir","errorCode":null,"errorMessage":"project_files.read_json path escapes project dir","messagePattern":"project_files\\.read_json path escapes project dir","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"apps/daemon/src/live-artifacts/refresh.ts","lineNumber":595,"sourceCode":"      mime: file.mime ?? 'application/octet-stream',\n    };\n    if (preview !== undefined) result.preview = preview;\n    matches.push(result);\n  }\n\n  return asBoundedRefreshOutput({ toolName: 'project_files.search', query: query ?? '', count: matches.length, truncated: allFiles.length > matches.length && matches.length >= maxResults, matches });\n}\n\nasync function executeProjectFilesReadJson(options: ExecuteLocalDaemonRefreshSourceOptions): Promise<BoundedJsonObject> {\n  const filePath = selectJsonPath(options.source.input as ProjectFilesReadJsonInput);\n  if (!filePath.endsWith('.json')) throw new Error('project_files.read_json only supports .json files');\n  const dir = projectDir(options.projectsRoot, options.projectId);\n  const target = path.resolve(dir, filePath);\n  const [dirReal, targetLinkStat] = await Promise.all([realpath(dir), lstat(target)]);\n  if (targetLinkStat.isSymbolicLink()) throw new Error('project_files.read_json does not follow symlinks');\n  const targetReal = await realpath(target);\n  if (!targetReal.startsWith(`${dirReal}${path.sep}`) && targetReal !== dirReal) {\n    throw new Error('project_files.read_json path escapes project dir');\n  }\n  const entryStat = await stat(targetReal);\n  if (!entryStat.isFile()) throw new Error('project_files.read_json path must be a file');\n  if (entryStat.size > 256 * 1024) throw new Error('project_files.read_json file exceeds 256KB');\n  if (options.signal?.aborted === true) throw options.signal.reason;\n  let parsed: BoundedJsonValue;\n  try {\n    parsed = JSON.parse(await readFile(targetReal, 'utf8')) as BoundedJsonValue;\n  } catch {\n    throw new Error(`project_files.read_json could not parse JSON at ${filePath}`);\n  }\n  return asBoundedRefreshOutput({ toolName: 'project_files.read_json', path: filePath, size: entryStat.size, json: parsed });\n}\n\nfunction compactExecOutput(value: string): string[] {\n  return value.split('\\n').map((line) => line.trimEnd()).filter(Boolean).slice(0, 100);\n}\n","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/refresh.ts#L577-L613","documentation":"executeProjectFilesReadJson (refresh.ts:593-596) computes the realpath of both the project directory and the target file, and requires the target's realpath to be the project dir itself or begin with `<projectReal>${path.sep}`. This blocks path-traversal attempts (../, absolute paths, or symlinks that escape the project root).","triggerScenarios":"input.path resolves to a real location outside the project directory, e.g. '../../../etc/passwd', an absolute path like '/etc/secrets.json', or a path that (after realpath) lands outside the project.","commonSituations":"Relative traversal in the path; an absolute path supplied by mistake or malice; a directory layout where the project dir is itself a symlink whose realpath differs from the supplied projectsRoot.","solutions":["Use a path that stays inside the project directory (relative, no '..' escapes).","Verify projectsRoot is the realpath of the project dir; if it is a symlink, resolve it before calling refresh.","Reject paths containing '..' or absolute paths at the input boundary."],"exampleFix":"// before\nconst input = { path: '../../../shared/config.json' };\n// throws `project_files.read_json path escapes project dir`\n\n// after: keep the file inside the project\nconst input = { path: 'config.json' };","handlingStrategy":"validation","validationCode":"import { realpath } from 'node:fs/promises';\nimport path from 'node:path';\nasync function assertInsideProject(dir: string, target: string): Promise<void> {\n  const [dirReal, targetReal] = await Promise.all([realpath(dir), realpath(target)]);\n  if (targetReal !== dirReal && !targetReal.startsWith(dirReal + path.sep)) {\n    throw new Error('path escapes project dir');\n  }\n}","typeGuard":"function isRelativeInside(p: string): boolean {\n  return !path.isAbsolute(p) && !p.split(path.sep).includes('..');\n}","tryCatchPattern":null,"preventionTips":["Reject absolute paths and '..' segments at the input boundary.","Ensure projectsRoot is the realpath of the project dir before refresh.","Treat any user-supplied file path as untrusted and confine it to the project root."],"tags":["live-artifacts","project-files","path-traversal","security"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}