{"record":{"id":"91b6053798866304","repo":"Yeachan-Heo/oh-my-codex","slug":"artifact-resolved-outside-working-directory","errorCode":null,"errorMessage":"artifact resolved outside working directory","messagePattern":"artifact resolved outside working directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mcp/hermes-bridge.ts","lineNumber":523,"sourceCode":"    throw new Error(\"artifact path must not traverse directories\");\n  }\n  if (!SAFE_ARTIFACT_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {\n    throw new Error(`artifact path must be under ${SAFE_ARTIFACT_PREFIXES.join(\", \")}`);\n  }\n  return normalized;\n}\n\nfunction isInsideDirectory(parent: string, candidate: string): boolean {\n  const rel = relative(parent, candidate);\n  return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nasync function resolveSafeArtifactPath(cwd: string, rel: string): Promise<string> {\n  const cwdRealPath = await realpath(cwd);\n  const full = resolve(cwd, rel);\n  const relativeToCwd = relative(resolve(cwd), full);\n  if (relativeToCwd.startsWith(\"..\") || isAbsolute(relativeToCwd)) {\n    throw new Error(\"artifact resolved outside working directory\");\n  }\n\n  let artifactRealPath: string;\n  try {\n    artifactRealPath = await realpath(full);\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code === \"ENOENT\") throw new Error(\"artifact_missing\");\n    throw error;\n  }\n\n  if (!isInsideDirectory(cwdRealPath, artifactRealPath)) {\n    throw new Error(\"artifact resolved outside working directory\");\n  }\n\n  for (const prefix of SAFE_ARTIFACT_PREFIXES) {\n    const rootRealPath = await realpath(resolve(cwd, prefix)).catch(() => null);\n    if (rootRealPath && isInsideDirectory(rootRealPath, artifactRealPath)) return artifactRealPath;\n  }","sourceCodeStart":505,"sourceCodeEnd":541,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/mcp/hermes-bridge.ts#L505-L541","documentation":"Thrown by resolveSafeArtifactPath when resolving the artifact path against the working directory produces a path that escapes it (relativeToCwd starts with \"..\" or is absolute). This is a defense-in-depth check after lexical normalization, before any filesystem access.","triggerScenarios":"A relative path that survives the earlier checks but still resolves outside cwd, e.g. via symlink-like segments or a race where cwd changed; effectively any path whose resolve(cwd, rel) sits outside cwd.","commonSituations":"Unusual path inputs like \"a/../../..\"; working directories that are themselves symlinks causing resolve/relative mismatches; crafted inputs in security testing.","solutions":["Simplify the path before sending (path.normalize) and drop any leading ../","Ensure the artifact genuinely lives inside the working directory","Verify the workingDirectory argument itself is the project root you expect"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const n = path.normalize(p); if (n.startsWith('..') || path.isAbsolute(n)) throw new Error('escapes cwd');","typeGuard":null,"tryCatchPattern":"try { await resolveArtifact(cwd, p); } catch (e) { if ((e as Error).message === 'artifact resolved outside working directory') {/* re-request a safe path */} throw e; }","preventionTips":["path.normalize inputs before sending","Confirm workingDirectory points at the real project root"],"tags":["path-traversal","security","mcp","artifact"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}