{"record":{"id":"467ac49aa27eba98","repo":"musistudio/claude-code-router","slug":"input-image-is-outside-allowed-roots-resolved","errorCode":null,"errorMessage":"Input image is outside allowed roots: ${resolved}","messagePattern":"Input image is outside allowed roots: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/media/service.ts","lineNumber":526,"sourceCode":"  }\n\n  private cleanup(): void {\n    const now = Date.now();\n    for (const job of this.jobStore.list()) {\n      if (job.artifact && Date.parse(job.artifact.expiresAt) <= now) this.artifactStore.delete(job.artifact);\n    }\n    for (const job of this.jobStore.deleteOlderThan(now - jobRetentionDays * 24 * 60 * 60 * 1000)) this.artifactStore.delete(job.artifact);\n  }\n\n  private validateImages(value: unknown, min: number, max: number): string[] {\n    const raw = typeof value === \"string\" ? [value] : Array.isArray(value) ? value : [];\n    if (raw.length < min || raw.length > max || raw.some((item) => typeof item !== \"string\" || !item.trim())) {\n      throw new Error(`images must contain between ${min} and ${max} local image paths.`);\n    }\n    const roots = mediaInputRoots(this.requireRuntimeConfig().allowedInputRoots);\n    return raw.map((item) => {\n      const resolved = realpathSync(expandHome(String(item).trim()));\n      if (!roots.some((root) => isPathInside(resolved, root))) throw new Error(`Input image is outside allowed roots: ${resolved}`);\n      const stats = statSync(resolved);\n      if (!stats.isFile() || stats.size <= 0 || stats.size > maxInputBytes) throw new Error(`Input image must be a non-empty regular file no larger than ${maxInputBytes} bytes.`);\n      if (!detectMediaType(resolved).mimeType?.startsWith(\"image/\")) throw new Error(`Unsupported input image format: ${resolved}`);\n      return resolved;\n    });\n  }\n\n  private finishCanceled(job: MediaJob, message: string): MediaJob {\n    const next = this.jobStore.update(job.id, {\n      error: { code: \"canceled\", message, retryable: false },\n      finishedAt: new Date().toISOString(),\n      status: \"canceled\"\n    });\n    this.completions.get(job.id)?.resolve(next);\n    this.completions.delete(job.id);\n    return next;\n  }\n","sourceCodeStart":508,"sourceCodeEnd":544,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/media/service.ts#L508-L544","documentation":"After resolving symlinks and ~ expansion, each input image path must fall inside one of the runtime config's allowedInputRoots. This is a path-traversal guard: realpath resolution defeats ../ and symlink escapes, so only genuinely whitelisted locations are accepted.","triggerScenarios":"Passing an image path outside allowedInputRoots, or a path that resolves (via symlink or ~) to a location outside the whitelist.","commonSituations":"Default allowed roots don't include /tmp or the project dir; user home shorthand points elsewhere; images placed next to configs in a non-whitelisted directory.","solutions":["Add the directory containing your images to allowedInputRoots in runtime config","Move/copy images into an already-allowed root and pass the resolved path","Use absolute real paths (no symlinks) pointing inside a whitelisted root"],"exampleFix":"// before\n{ allowedInputRoots: [\"/var/media\"] }\n// images: \"/home/user/pic.png\"\n// after\n{ allowedInputRoots: [\"/var/media\", \"/home/user\"] }","handlingStrategy":"validation","validationCode":"import { realpathSync } from \"node:fs\";\nconst resolved = realpathSync(inputPath);\nif (!roots.some(r => isPathInside(resolved, r))) throw new Error(\"outside roots\");","typeGuard":"const isInsideRoots = (p: string, roots: string[]): boolean => roots.some(r => !path.relative(path.resolve(r), path.resolve(p)).startsWith(\"..\"));","tryCatchPattern":"try { await call(); } catch (e) { if (e instanceof Error && e.message.includes(\"outside allowed roots\")) return configError(); throw e; }","preventionTips":["Stage inputs under a whitelisted directory","Resolve symlinks before passing paths"],"tags":["media","security","path-validation"],"backgroundTag":"path-outside-allowed-root","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}