{"record":{"id":"c0fc0acd5c7f76fb","repo":"stablyai/orca","slug":"path-filepath-resolves-outside-the-worktree","errorCode":null,"errorMessage":"Path \"${filePath}\" resolves outside the worktree","messagePattern":"Path \"(.+?)\" resolves outside the worktree","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/main/git/status.ts","lineNumber":2138,"sourceCode":"  } finally {\n    invalidateGitReadCaches()\n  }\n}\n\n/**\n * Discard working tree changes for a file.\n */\nexport async function discardChanges(\n  worktreePath: string,\n  filePath: string,\n  options: GitRuntimeOptions = {}\n): Promise<void> {\n  invalidateGitReadCaches()\n  const resolvedWorktree = path.resolve(worktreePath)\n  const resolvedTarget = path.resolve(worktreePath, filePath)\n  try {\n    if (!isWithinWorktree(path, resolvedWorktree, resolvedTarget)) {\n      throw new Error(`Path \"${filePath}\" resolves outside the worktree`)\n    }\n\n    let tracked = false\n    try {\n      await gitExecFileAsync(\n        ['ls-files', '--error-unmatch', '--', literalPathspec(filePath, options)],\n        {\n          ...gitOptionsForWorktree(worktreePath, options)\n        }\n      )\n      tracked = true\n    } catch {\n      // File is not tracked by git\n    }\n\n    if (tracked) {\n      await gitExecFileAsync(\n        ['restore', '--worktree', '--source=HEAD', '--', literalPathspec(filePath, options)],","sourceCodeStart":2120,"sourceCodeEnd":2156,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/git/status.ts#L2120-L2156","documentation":"discardChanges resolves the target path against the worktree, then calls isWithinWorktree; if the resolved target equals the worktree root or escapes it (relative is '', '..', or starts with '..<sep>'), it throws before touching git. Without this guard, git restore / git clean could be pointed at files outside the worktree, so it is a security boundary. Note that relative '' (target is the worktree itself) is rejected — you cannot discard the worktree root.","triggerScenarios":"Calling discardChanges(worktreePath, filePath) where filePath is '', '.', '..', an absolute path outside the worktree, or a relative path that climbs above the worktree root (e.g. '../../etc/passwd'). Also when filePath resolves to the worktree directory itself.","commonSituations":"A UI discard action that sent an empty or stale file path; a path that was pre-resolved to absolute by the renderer; adversarial IPC input; symlinks inside the worktree pointing outside (resolve() follows them); race where the worktree moved between path capture and the discard call.","solutions":["Send file paths relative to the worktree, as git reports them, and ensure they are non-empty.","Reject '', '.', and '..' at the renderer before issuing a discard IPC.","If symlinks inside the worktree point outside, treat the discard as unsupported for those entries rather than letting resolve() escape.","Use isWithinWorktree (exported) at the caller to pre-screen bulk paths."],"exampleFix":"// before\ndiscardChanges(worktreePath, rawPath)\n\n// after: pre-screen at the caller\nimport { isWithinWorktree } from './status'\nimport path from 'node:path'\nconst resolvedTarget = path.resolve(worktreePath, rawPath)\nif (!rawPath || !isWithinWorktree(path, path.resolve(worktreePath), resolvedTarget)) {\n  throw new Error('Refuse to discard a path outside the worktree.')\n}\ndiscardChanges(worktreePath, rawPath)","handlingStrategy":"validation","validationCode":"import path from 'node:path'\nimport { isWithinWorktree } from './status'\n\nfunction isSafeDiscardTarget(worktreePath: string, filePath: string): boolean {\n  if (!filePath) return false\n  const root = path.resolve(worktreePath)\n  const target = path.resolve(root, filePath)\n  return isWithinWorktree(path, root, target)\n}\n\nif (!isSafeDiscardTarget(worktreePath, filePath)) throw new Error('Refuse to discard a path outside the worktree.')","typeGuard":"function isDiscardOutsideWorktree(error: unknown): boolean {\n  return error instanceof Error && /^Path \".+\" resolves outside the worktree$/.test(error.message)\n}","tryCatchPattern":"if (!isSafeDiscardTarget(worktreePath, filePath)) {\n  showStatus('Cannot discard: path is outside the worktree.')\n  return\n}\ntry { await discardChanges(worktreePath, filePath, options) }\ncatch (error) { if (isDiscardOutsideWorktree(error)) { showStatus('Cannot discard: path is outside the worktree.'); return } throw error }","preventionTips":["Send file paths to discard as git reports them (relative to the worktree), never absolute.","Reject '', '.', and '..' at the renderer before issuing the discard IPC.","Pre-screen with the exported isWithinWorktree helper at the caller."],"tags":["git","discard","security","path-traversal","filesystem"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}