{"record":{"id":"b7a3b36b66c71904","repo":"stablyai/orca","slug":"access-denied-path-resolves-outside-allowed-direc","errorCode":null,"errorMessage":"Access denied: path resolves outside allowed directories. If this blocks a legitimate workflow, please file a GitHub issue.","messagePattern":"Access denied: path resolves outside allowed directories\\. If this blocks a legitimate workflow, please file a GitHub issue\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/main/ipc/filesystem-auth.ts","lineNumber":296,"sourceCode":"    error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT'\n  )\n}\n\nexport type ResolveAuthorizedPathOptions = {\n  /**\n   * Canonicalize the parent but preserve the leaf so delete/rename target the symlink itself, not its destination (which may live outside allowed roots).\n   */\n  preserveSymlink?: boolean\n}\n\nexport async function resolveAuthorizedPath(\n  targetPath: string,\n  store: Store,\n  options: ResolveAuthorizedPathOptions = {}\n): Promise<string> {\n  const resolvedTarget = resolve(targetPath)\n  if (!(await isPathAllowedIncludingRegisteredWorktrees(resolvedTarget, store))) {\n    throw new Error(PATH_ACCESS_DENIED_MESSAGE)\n  }\n\n  if (options.preserveSymlink) {\n    // Canonicalize the parent so ancestor symlinks can't redirect outside allowed roots, but keep the leaf so delete/rename act on the link itself.\n    let realParent: string\n    try {\n      realParent = await realpath(dirname(resolvedTarget))\n    } catch (error) {\n      if (isENOENT(error)) {\n        return resolveAuthorizedMissingPath(resolvedTarget, store)\n      }\n      throw error\n    }\n    const candidateTarget = resolve(realParent, basename(resolvedTarget))\n    if (\n      !(await isPathAllowedIncludingRegisteredWorktrees(candidateTarget, store, {\n        canonicalSourcePath: resolvedTarget\n      }))","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/filesystem-auth.ts#L278-L314","documentation":"Primary filesystem-auth boundary in `resolveAuthorizedPath` (default, non-symlink-preserving branch): after `resolve(targetPath)`, the resolved target is not inside any allowed directory, registered worktree, or canonical allowed/registered root (checked via `isPathAllowedIncludingRegisteredWorktrees`). The message includes guidance to file an issue because the check is deliberately conservative.","triggerScenarios":"Calling `resolveAuthorizedPath(path, store)` with a path whose resolved form is outside every repo root, folder-workspace root, project-group subtree, registered worktree, and externally-authorized path.","commonSituations":"Renderer passes an absolute path from outside the workspace (e.g. `/etc/hosts`); a symlink inside the allowed root points outside it; path computed against the wrong workspace; a newly added repo whose roots cache has not been refreshed.","solutions":["Confirm the path is inside a registered repo/folder-workspace root or a registered git worktree.","If the path is legitimately external and user-authorized, call `authorizeExternalPath(path)` before resolving.","Call `invalidateAuthorizedRootsCache()` after repo/worktree registration changes, then retry."],"exampleFix":"// before\nconst p = await resolveAuthorizedPath('/etc/hosts', store)\n\n// after\nconst p = await resolveAuthorizedPath(join(repoPath, 'README.md'), store)\n// or, for a legitimately external user-chosen path:\nauthorizeExternalPath(userChosenExternalPath)\nconst p = await resolveAuthorizedPath(userChosenExternalPath, store)","handlingStrategy":"validation","validationCode":"// Before resolving, confirm the path is inside an allowed root or pre-authorize it.\nimport { isPathInsideOrEqual } from '../../shared/cross-platform-path'\nconst resolved = resolve(targetPath)\nconst allowedRoots = getAllowedRoots(store) // repo/folder-workspace/project-group roots\nif (!allowedRoots.some((root) => isPathInsideOrEqual(resolved, root))) {\n  authorizeExternalPath(resolved) // only if the user explicitly approved this external path\n}\nawait resolveAuthorizedPath(targetPath, store)","typeGuard":"function isInsideAnyRoot(p: string, roots: string[]): boolean {\n  return roots.some((root) => isPathInsideOrEqual(p, root))\n}","tryCatchPattern":"try {\n  return await resolveAuthorizedPath(targetPath, store)\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Access denied: path resolves outside')) {\n    // surface to user; only authorize external path on explicit consent\n    throw new PermissionDeniedError(e.message)\n  }\n  throw e\n}","preventionTips":["Always derive paths from registered repo/folder-workspace roots rather than user-typed strings.","Call invalidateAuthorizedRootsCache() after registration changes so new roots are honored.","Use authorizeExternalPath only for paths the user explicitly chose via a native picker."],"tags":["filesystem","security-boundary","path-traversal","authorization","ipc"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}