Repository navigation
resolveWithin accepts an existing sibling path that shares the root's string prefix #22826
Description
Summary
resolveWithin() documents that its result is guaranteed to reside at or below the supplied root. Its final containment check compares canonical paths using realResolved.startsWith(realRoot). This is a string-prefix check rather than a path-component boundary check, so an existing sibling such as /tmp/repo-backup/file is accepted for the root /tmp/repo.
This is a path-helper correctness and defense-in-depth issue. I have not established a GitHub Desktop remote-code-execution path through it.
Environment
- Release/build: GitHub Desktop development source at revision
57d54221902278602d34579d218fea414229ffde(not a packaged release) - Operating system: Windows 11 Pro, version 24H2, build 26100.9278 (x64)
Relevant code
Observed at revision 57d5422 in app/src/lib/path.ts:68-71:
const realRoot = await realpath(normalizedRoot)
const realResolved = await realpath(resolved)
return realResolved.startsWith(realRoot) ? resolved : null
Source:
Lines 68 to 71 in 57d5422
| const realRoot = await realpath(normalizedRoot) | |
| const realResolved = await realpath(resolved) | |
| return realResolved.startsWith(realRoot) ? resolved : null |
Safe reproduction
- Create a temporary parent directory.
- Create existing children named repo and repo-sibling.
- Create repo-sibling/marker.txt.
- Call resolveWithin(repo, '../repo-sibling/marker.txt').
Expected: null.
Actual: the canonical sibling path is returned because its string begins with the canonical root string.
No symlink, special permission, external host, or destructive action is required.
Impact and limitations
- The helper's documented containment guarantee is false for prefix-colliding sibling names.
- Current and future callers can receive an out-of-root path.
- A reviewed deep-link caller passes the result to shell.showItemInFolder; that is not code execution.
- The reviewed Copilot conflict-resolution path has separate conflict-path validation. I did not demonstrate an arbitrary-file write.
- This is not presented as a bounty-eligible vulnerability.
Suggested remediation
Compare path components rather than raw prefixes. For example, compute the relative path from realRoot to realResolved using the same platform-specific path implementation, then accept only the empty relative path or a non-absolute path whose first component is not '..'. Please add regression coverage for prefix-colliding siblings on POSIX and Windows.
I used AI-assisted tooling to support source-code review, hypothesis generation, duplicate searching, and report drafting. I reviewed the resulting analysis, verified claims against the cited source code, and reproduced behaviors locally where stated. No report was submitted or external system modified autonomously by the AI.
Activity
desktop-triage commentedon Sep 7, 2026
Triage summary: The code at the cited location confirms the report:
resolveWithin's containment check usesrealResolved.startsWith(realRoot), a raw string-prefix comparison rather than a path-segment boundary check, so a sibling directory whose name has the root's name as a prefix (e.g.repo-siblingvsrepo) would pass containment. I'm suggestingbugandpriority-3, since this is a defense-in-depth correctness issue in a path-safety helper rather than a demonstrated RCE/data-loss path — the reporter explicitly notes no arbitrary-file-write or code-execution path was established, and the one traced caller only passes the result toshell.showItemInFolder.Initial hypothesis: The bug is a classic path-prefix-without-separator flaw: comparing
realResolved.startsWith(realRoot)treats/tmp/repo-siblingas "within"/tmp/repobecause the string/tmp/repois a literal prefix of/tmp/repo-sibling, even thoughrepo-siblingis not a subdirectory ofrepo. The fix suggested in the report (compute the relative path viapath.relativeand reject results that are absolute or start with.., or equivalently requirerealResolved === realRootorrealResolved.startsWith(realRoot + sep)) is a standard, low-risk correction. Confirmed inapp/src/lib/path.tslines 68-71 at commit57d54221902278602d34579d218fea414229ffde.Duplicate check: Searched for related reports on
resolveWithin/path traversal/sibling prefix containment issues in this repo; found none. No duplicate identified.