{"record":{"id":"4c9944a019086d7f","repo":"stablyai/orca","slug":"access-denied-invalid-submodule-path","errorCode":null,"errorMessage":"Access denied: invalid submodule path","messagePattern":"Access denied: invalid submodule path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/main/git/status.ts","lineNumber":503,"sourceCode":"            timeout: GIT_BRANCH_LINE_TOTAL_TIMEOUT_MS\n          }).then((result) => result.stdout),\n        ...(options.signal ? { signal: options.signal } : {})\n      })\n  }\n}\n\nfunction getStatusLineStatsCacheKey(worktreePath: string, options: GitRuntimeOptions = {}): string {\n  // Why: identical paths can map to different WSL-distro filesystems, so key stats by Git's execution host.\n  return `${options.wslDistro ?? 'native'}\\0${worktreePath}`\n}\n\n/**\n * Resolve a submodule's own worktree path from a parent worktree + relative\n * submodule path, rejecting anything that escapes the parent.\n */\nexport function resolveSubmoduleWorktreePath(worktreePath: string, submodulePath: string): string {\n  if (!submodulePath || submodulePath.includes('\\0') || path.isAbsolute(submodulePath)) {\n    throw new Error('Access denied: invalid submodule path')\n  }\n  const resolved = path.resolve(worktreePath, submodulePath)\n  const rel = path.relative(worktreePath, resolved)\n  if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {\n    throw new Error('Access denied: submodule path escapes the selected worktree')\n  }\n  return resolved\n}\n\n/**\n * Run a plain status inside a submodule's own worktree (lazy \"expand submodule\"\n * flow). Entry paths are relative to the submodule root; the renderer prefixes them.\n */\nexport async function getSubmoduleStatus(\n  worktreePath: string,\n  submodulePath: string,\n  options: GetStatusOptions & { staged?: boolean } = {}\n): Promise<GitStatusResult> {","sourceCodeStart":485,"sourceCodeEnd":521,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/git/status.ts#L485-L521","documentation":"resolveSubmoduleWorktreePath builds a submodule's own worktree path from a parent worktree plus a relative submodule path. It throws this message when submodulePath is empty, contains a NUL byte, or is an absolute path — none of those can be a valid relative submodule entry, and accepting them would let a caller target arbitrary filesystem locations. This is the input-shape guard; the escape guard at :508 is the second layer.","triggerScenarios":"Calling resolveSubmoduleWorktreePath(worktreePath, submodulePath) (directly, or via getSubmoduleStatus / loadDiff routing) with submodulePath = '', a path containing '\\0', or an absolute path like '/etc' or 'C:\\Windows'.","commonSituations":"A UI entry that fed an empty submodule path because the selection was not yet ready; a malformed .gitmodules entry that produced an empty path; adversarial or corrupted path input from an untrusted source; a path that was pre-resolved to absolute by a caller that did not realise the function expects a relative path.","solutions":["Pass a non-empty, relative submodule path as it appears in .gitmodules / git submodule status.","Reject empty or NUL-containing paths at the caller before invoking the function.","If you have an absolute path, compute the relative path against the parent worktree first (path.relative).","Do not feed user-typed arbitrary paths directly — source them from the submodule listing."],"exampleFix":"// before\nresolveSubmoduleWorktreePath(worktreePath, rawEntry)\n\n// after: validate shape and relativize\nif (!rawEntry || rawEntry.includes('\\0') || path.isAbsolute(rawEntry)) {\n  throw new Error('Submodule path must be a non-empty relative path.')\n}\nresolveSubmoduleWorktreePath(worktreePath, rawEntry)","handlingStrategy":"validation","validationCode":"import path from 'node:path'\n\nfunction isValidSubmodulePath(submodulePath: string): boolean {\n  return Boolean(submodulePath) && !submodulePath.includes('\\0') && !path.isAbsolute(submodulePath)\n}\n\nif (!isValidSubmodulePath(submodulePath)) throw new Error('Submodule path must be a non-empty relative path.')","typeGuard":"function isInvalidSubmodulePath(error: unknown): boolean {\n  return error instanceof Error && error.message === 'Access denied: invalid submodule path'\n}","tryCatchPattern":"if (!isValidSubmodulePath(submodulePath)) {\n  throw new Error('Refuse to load submodule: path is empty, absolute, or contains a NUL byte.')\n}\ntry { return resolveSubmoduleWorktreePath(worktreePath, submodulePath) }\ncatch (error) { if (isInvalidSubmodulePath(error)) { showStatus('Cannot open submodule: invalid path.'); return null } throw error }","preventionTips":["Always pass submodule paths as git reports them (relative, from .gitmodules / git submodule status).","Reject empty and NUL-containing paths at the caller boundary.","If you hold an absolute path, relativize it against the parent worktree first."],"tags":["git","submodule","security","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}