{"record":{"id":"6a5c1d7c5aebebb1","repo":"nexu-io/open-design","slug":"image-path-rel-resolves-outside-the-project","errorCode":null,"errorMessage":"--image path \"${rel}\" resolves outside the project directory.","messagePattern":"--image path \"(.+?)\" resolves outside the project directory\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":226,"sourceCode":"\n/**\n * Resolve a project-relative `--image` path into a base64 data URL the\n * upstream model APIs (Volcengine i2v, OpenAI image-edit, etc.) accept\n * directly. Returns null when no path was supplied.\n *\n * Security: refuses anything that escapes the project directory.\n * Without this guard, an agent (or a hallucinated arg) could ask the\n * daemon to upload `/etc/passwd` to a paid model.\n */\nasync function resolveProjectImage(rel: unknown, projectDir: string): Promise<ImageRef | null> {\n  if (typeof rel !== 'string' || !rel.trim()) return null;\n  const projectRootResolved = path.resolve(projectDir);\n  const abs = path.resolve(projectRootResolved, rel.trim());\n  if (\n    abs !== projectRootResolved &&\n    !abs.startsWith(projectRootResolved + path.sep)\n  ) {\n    throw new Error(\n      `--image path \"${rel}\" resolves outside the project directory.`,\n    );\n  }\n  let info;\n  try {\n    info = await stat(abs);\n  } catch {\n    throw new Error(`--image not found: ${rel}`);\n  }\n  if (!info.isFile()) {\n    throw new Error(`--image is not a regular file: ${rel}`);\n  }\n  // Cap at 16 MB. Beyond this, base64 inflation alone (≈4/3) starts\n  // hitting body-size limits at the upstream APIs and our own express\n  // 4mb body cap on inbound requests; bigger payloads should travel\n  // via the dedicated upload endpoint, not the dispatcher.\n  const MAX_IMAGE_BYTES = 16 * 1024 * 1024;\n  if (info.size > MAX_IMAGE_BYTES) {","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L208-L244","documentation":"Thrown by resolveProjectImage, a security guard in apps/daemon/src/media/index.ts. The resolved absolute path of an `--image` argument must equal or sit beneath the project directory; any path that escapes (via `..`, absolute paths, or symlink resolution) is rejected before stat/readFile. This blocks an agent or hallucinated arg from uploading arbitrary host files (e.g. /etc/passwd) to a paid model.","triggerScenarios":"Passing `--image ../../../etc/passwd` or `--image /etc/shadow`; a symlink inside the project that points outside; an absolute path that is not under the project root; path components that normalize above the project root.","commonSituations":"Agent hallucinates a system path as an image source; symlinked assets pointing outside the workspace; user pastes an absolute path from elsewhere on the machine.","solutions":["Reference images by a path relative to the project root and keep them inside the project directory.","Copy the external image into the project's assets folder first, then reference it.","Remove or relocate symlinks that escape the project root.","Audit agent-supplied --image args against the project root before dispatch."],"exampleFix":"// before\n--image /home/user/pic.png\n// after: place under project first\n--image assets/pic.png","handlingStrategy":"validation","validationCode":"const abs = path.resolve(projectDir, rel.trim());\nif (abs !== projectDir && !abs.startsWith(projectDir + path.sep)) {\n  throw new Error('image path escapes project dir');\n}","typeGuard":"function isWithinProject(rel: string, projectDir: string): boolean {\n  const abs = path.resolve(projectDir, rel.trim());\n  return abs === projectDir || abs.startsWith(projectDir + path.sep);\n}","tryCatchPattern":null,"preventionTips":["Keep all image assets under the project directory.","Reject agent-supplied absolute paths before dispatch.","Resolve and audit symlinks inside asset folders."],"tags":["media","security","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}