{"record":{"id":"64cc611b1f3407e8","repo":"Yeachan-Heo/oh-my-codex","slug":"native-assets-cache-path-escapes-configured-root","errorCode":null,"errorMessage":"[native-assets] cache path escapes configured root","messagePattern":"\\[native-assets\\] cache path escapes configured root","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/native-assets.ts","lineNumber":364,"sourceCode":"\nfunction lockRecord(token: string, binaryPath: string): string {\n  return `${JSON.stringify({\n    version: 1,\n    token,\n    pid: process.pid,\n    hostname: hostname(),\n    started_at: new Date().toISOString(),\n    binary_path: binaryPath,\n  })}\\n`;\n}\n\n\nfunction canonicalDescendantPath(path: string, configuredRoot: string, canonicalRoot: string): string {\n  const resolvedPath = resolve(path);\n  const canonicalRelative = relative(canonicalRoot, resolvedPath);\n  if (canonicalRelative && canonicalRelative !== '..' && !canonicalRelative.startsWith(`..${sep}`)) return resolvedPath;\n  const rel = relative(resolve(configuredRoot), resolvedPath);\n  if (!rel || rel === '..' || rel.startsWith(`..${sep}`)) throw new Error('[native-assets] cache path escapes configured root');\n  return join(canonicalRoot, rel);\n}\n\nasync function canonicalCacheRoot(root: string, create: boolean): Promise<string | undefined> {\n  try {\n    if (create) await mkdir(root, { recursive: true, mode: 0o700 });\n    const entry = await lstat(root);\n    if (!entry.isDirectory() && !entry.isSymbolicLink()) throw new Error('unsafe root');\n    return await realpath(root); // The configured root itself is intentionally allowed to be a symlink.\n  } catch (error) {\n    if (!create && absent(error)) return undefined;\n    throw error;\n  }\n}\n\nasync function validateDescendant(path: string, canonicalRoot: string, createParents: boolean): Promise<void> {\n  const candidate = resolve(path);\n  const rel = relative(canonicalRoot, candidate);","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/native-assets.ts#L346-L382","documentation":"A caller-supplied cache path resolved outside the configured native cache root. The library canonicalizes the cache root (realpath) and requires every managed path to be a descendant; if the path is outside even after resolving symlinks/canonicalization, publication/inspection is refused to prevent writes outside the cache.","triggerScenarios":"inspectManagedNativeBinary or publishManagedNativeBinary with a destination that, once resolved, is not under the canonicalized cache root — e.g. a symlink inside the cache pointing elsewhere, or a custom cache-root env var (NATIVE_CACHE_ROOT-style override) that doesn't contain the destination path.","commonSituations":"Users overriding the cache root env var but passing hardcoded paths; symlinks inside the cache directory (dotfiles managers like Stow, tmpfs symlinks); CI caches that replace directories with links.","solutions":["Pass destinations derived from the cache root the library itself resolves, not absolute hardcoded paths.","Remove symlinks inside the cache directory so realpath stays within the root.","Align the cache-root env override with the paths you pass in."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { resolve, relative } from 'node:path';\nfunction isInside(root: string, p: string): boolean {\n  const rel = relative(resolve(root), resolve(p));\n  return rel !== '' && rel !== '..' && !rel.startsWith(`..${require('node:path').sep}`);\n}","typeGuard":null,"tryCatchPattern":"try { await inspectManagedNativeBinary(dest); } catch (e) { if (/cache path escapes configured root/.test(String(e))) { /* derive dest from the library's cache root */ } throw e; }","preventionTips":["Never hardcode absolute cache paths","Derive destinations from the cache root the library resolves","Keep symlinks out of the cache directory"],"tags":["native-assets","cache","path-traversal","filesystem-security"],"backgroundTag":"path-escapes-root","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}