Visitar URL original
`resolveWithin` accepts an existing sibling path that shares the root's string prefix · Issue #22826 · desktop/desktop · GitHub
Skip to content

resolveWithin accepts an existing sibling path that shares the root's string prefix #22826

Open

Description

@arthurianresolve

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:

const realRoot = await realpath(normalizedRoot)
const realResolved = await realpath(resolved)
return realResolved.startsWith(realRoot) ? resolved : null

Safe reproduction

  1. Create a temporary parent directory.
  2. Create existing children named repo and repo-sibling.
  3. Create repo-sibling/marker.txt.
  4. 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

  1. desktop-triage commented on Sep 7, 2026

    @desktop-triage

    Triage summary: The code at the cited location confirms the report: resolveWithin's containment check uses realResolved.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-sibling vs repo) would pass containment. I'm suggesting bug and priority-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 to shell.showItemInFolder.

    Initial hypothesis: The bug is a classic path-prefix-without-separator flaw: comparing realResolved.startsWith(realRoot) treats /tmp/repo-sibling as "within" /tmp/repo because the string /tmp/repo is a literal prefix of /tmp/repo-sibling, even though repo-sibling is not a subdirectory of repo. The fix suggested in the report (compute the relative path via path.relative and reject results that are absolute or start with .., or equivalently require realResolved === realRoot or realResolved.startsWith(realRoot + sep)) is a standard, low-risk correction. Confirmed in app/src/lib/path.ts lines 68-71 at commit 57d54221902278602d34579d218fea414229ffde.

    Duplicate check: Searched for related reports on resolveWithin/path traversal/sibling prefix containment issues in this repo; found none. No duplicate identified.

    Generated by Issue Triage (skills-driven) for #22826 · auto · 21.6 AIC · ⌖ 1.12 AIC · ⊞ 9.3K · ◷

  2. added
    bugConfirmed bugs or reports that are very likely to be bugs
    priority-3Bugs that affect small number of users and/or relatively cosmetic in nature
    on Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed bugs or reports that are very likely to be bugspriority-3Bugs that affect small number of users and/or relatively cosmetic in nature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions