Skip to content

fix: isolate self-upgrade installer lookup from project paths - #5175

Open
mnriem wants to merge 10 commits into
github:mainfrom
mnriem:mnriem-installer-lookup-security
Open

mnriem wants to merge 10 commits into
github:mainfrom
mnriem:mnriem-installer-lookup-security

Conversation

@mnriem

@mnriem mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Resolve uv and pipx installers from absolute PATH directories outside the current working directory and Git worktree. Use the same lookup for registry detection and self-upgrades, bypassing Windows' implicit current-directory search while preserving eligible PATH ordering and PATHEXT handling.

Add regression coverage for unsafe search paths, symlinked directories, absolute installer execution, missing-installer errors, and normal user-local installations.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

The full suite used the documented worktree-local interpreter instead of the generic command above:

  • uv sync --extra test --quiet — passed.
  • LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q — passed: 13,702 passed, 130 skipped.
  • LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_version_*.py tests/specify_cli/selfs tests/specify_cli/test_root_version_callback.py -q --tb=short — passed: 212 passed, 1 skipped.
  • uv run specify --help — passed.

Run on macOS; native Windows validation was not performed. Sample-project testing is not applicable because this change does not affect scaffolding or generated project assets.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: GitHub Copilot using GPT-6.1 Sol in autonomous mode generated the implementation, tests, commit message, and pull request body, and ran validation. The initial alert assessment used a security-review subagent whose model was not reported by the runtime. No documentation changes are included.

Resolve uv and pipx through absolute PATH directories outside the working directory and Git worktree for registry probes and self-upgrades. Preserve Windows PATHEXT handling and eligible PATH ordering, with regression coverage for unsafe search paths and normal installations.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical symlink bypasses in executable lookup allow registry detection and self-upgrade to execute project-controlled code despite worktree exclusions.

2 open issues
What changed in this PR

This PR centralizes installer lookup for Specify CLI’s registry detection and self-upgrade workflow to reduce execution of project-controlled uv and pipx binaries.

Changes:

  • Filters installer searches to eligible absolute PATH directories, preserving ordering and Windows PATHEXT handling.
  • Shares lookup between registry probes and upgrade commands.
  • Adds regression coverage for unsafe paths, symlinks, absolute execution, and missing installers.

AI-assisted review for the requester: GitHub Copilot; model unknown; autonomous analysis, suggested fixes, and summary.

File Description
tests/​specify_cli/​test_version_execution.py Updates execution-test mocking to use the shared installer lookup.
tests/​specify_cli/​test_version_detection.py Adds lookup security regressions and adapts existing mocks.
src/​specify_cli/​_version.py Adds filtered installer discovery and uses it for detection and upgrades.

Review effort: Max (auto)

Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 29, 2026
Validate directory ancestors and intermediate symlink targets, and reject installer executables resolving into the working directory or Git worktree. Execute the validated absolute target and use absolute installer fixtures in lookup mocks. Add regression coverage for alias, file, and combined symlink bypasses across discovery and upgrades.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 13:38
@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both findings in ee20e70: installer lookup now checks directory ancestors and intermediate symlink targets, rejects executables resolving into the working directory or Git worktree, and executes the validated absolute target. Lookup mocks now return absolute installer paths, with regression coverage for alias, file, and combined symlink bypasses across lookup, registry detection, and upgrades. LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q -n 2 passed: 13,756 passed, 130 skipped. Validation ran on macOS; native Windows validation was not performed. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous mode), which authored the implementation, tests, and this summary and ran validation.

Reuse the existing comparison-only extended-path normalization for installer cwd and worktree checks. Preserve original filesystem access paths while recognizing equivalent Windows drive and UNC namespace spellings, including intermediate symlink targets. Add portable containment regressions for plain and extended roots and targets.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical symlink, junction, and platform-specific path-comparison bypasses still allow project- or CWD-controlled paths to influence installer selection.

6 open issues
2 resolved since last review

Review effort: Max (auto)

Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Copilot AI auto review requested due to automatic review settings October 29, 2026 14:03
Inspect intermediate executable links even without a Git root, compare filesystem identities for cwd and worktree boundaries, and reject unsupported Windows reparse points. Warn and continue after per-entry inspection failures while preserving the selected absolute installer shim spelling. Add case-alias, CWD-chain, dispatcher, failure-recovery, and native Windows namespace/junction regressions.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed this review in cd1e7d2 and b98bc93. Installer lookup now inspects intermediate executable links even without a Git root, uses filesystem identities for CWD and worktree boundaries, recognizes equivalent Windows namespace spellings, rejects unsupported reparse points including junctions, warns and continues after per-entry inspection failures, and executes the selected absolute shim spelling rather than its dispatcher target. Added regressions for CWD link chains, case-insensitive aliases, inspection failures, differently named dispatchers, and native Windows namespace/junction paths. The focused local tests passed: 489 passed, 66 skipped. CI has passed on Linux and macOS with Python 3.13/3.14 and Windows with Python 3.14; Windows Python 3.13 is still running. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous mode), which authored the implementation, tests, and this summary and ran validation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Installer validation still permits working-directory-controlled symlink chains and project-controlled Windows junctions, while returning resolved targets breaks symlink-based dispatch.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

High severity Detect Windows junctions in installer symlink validation

src/​specify_cli/​_version.py:502

Windows junctions are not detected by is_symlink(), so a chain such as C:\alias -> C:\project\bin -> C:\external passes these checks despite traversing a project-controlled junction. Registry probes and upgrades can therefore use an installer selected through the worktree. Inspect junction targets or reject non-symlink reparse points as src/specify_cli/file_helper.py:79-88 does, and add a native Windows regression.

High severity Validate intermediate symlinks without a Git ancestor

src/​specify_cli/​_version.py:508

Without a Git ancestor, this early return accepts an external bin/uv -> $PWD/uv -> /outside/uv chain because only the endpoint parents are checked. The working directory therefore still controls installer selection. Traverse intermediate symlinks even without a worktree and reject links located directly in cwd, while preserving allowed user-local descendant directories; add regressions for both installers.

Medium severity Return the validated entrypoint for symlink dispatch

src/​specify_cli/​_version.py:545

Returning the resolved target changes the executable name for symlink-based dispatchers, so a valid bin/uv -> dispatcher runs in the wrong mode and breaks registry detection and self-upgrade. Return the validated absolute entrypoint instead, and update the external-file-symlink test to check dispatch behavior rather than require the target path.

6 resolved since last review

Review effort: Max (auto)

Copilot AI auto review requested due to automatic review settings October 29, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Installer resolution still allows CWD-controlled directory-symlink chains to override trusted installers when no Git ancestor exists, while blanket junction rejection also blocks legitimate Windows installations.

1 open issue

Review effort: Max (auto)

Comment thread src/specify_cli/_version.py Resolved
Recognize directory mount-point reparse tags and inspect their targets instead of rejecting every Windows junction. Keep unsupported reparse types and CWD-controlled intermediate directory links excluded. Add portable and native Windows coverage for safe external installer paths, registry detection, upgrades, and unsafe junction chains.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Installer discovery can reject valid symlinked installations, crash from a deleted working directory, or select the wrong Windows executable, requiring human assessment.

No open issues 🎉

1 resolved since last review

Review effort: Max (auto)

@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in 1951d9f. Supported external directory junctions now receive target and ancestry validation instead of blanket rejection, preserving installer paths such as current\Scripts for lookup, registry detection, and upgrades. Unsupported reparse types, worktree crossings, and CWD-controlled intermediate file or directory links remain excluded. Added portable and native Windows acceptance/rejection regressions, including directory-link chains outside Git projects. The focused local tests passed: 509 passed, 76 skipped. Native Windows CI for this commit is pending. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous mode), which authored the implementation, tests, and this summary and ran validation.

Reconcile the merged installer security fix with absolute PATH lookup, filesystem-identity validation, and safe external junction support. Preserve upstream rejection diagnostics, ambiguous-probe handling, and CLI exit behavior. Retain both branches' regression inventories and adapt mocks to the typed detection result and absolute lookup inputs.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Installer discovery can reject legitimate symlinked installations, let non-executable PATH entries block valid matches, and select the wrong Windows executable, while relative-symlink trust checks lack test coverage.

4 open issues

Review effort: Max (auto)

Comment thread src/specify_cli/_version.py Resolved
(
target
if target.is_absolute()
else ancestor.parent / target,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relative targets are now exercised by test_relative_installer_links_preserve_trust_boundaries: an external ../venvs/... target is accepted, while relative chains entering the CWD or Git worktree are rejected. The home-directory fixtures also combine relative directory and installer targets, including ../Cellar/.... The native Windows run executed all six boundary cases and all 24 home-directory cases without skips. If you had another relative-target layout in mind, could you point to it? AI-assisted reply on behalf of @mnriem: GitHub Copilot (GPT-6.1 Sol, autonomous mode); analysis, test changes, and this reply.

Comment thread src/specify_cli/_version.py Resolved
Comment thread src/specify_cli/_version.py Resolved
Allow PATH-declared external directory links and relative installer targets without permitting indirect CWD or worktree crossings. Ignore non-executable relative PATH matches while preserving their spelling in rejection diagnostics. Expand PATHEXT manually only on Python 3.11 and let newer Python perform a single expansion. Add acceptance, rejection, and native competing-suffix regressions.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 30, 2026 12:19
@mnriem

mnriem commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Addressed all four findings in 2c09108, with Windows parent-component handling corrected in 520e072. Explicitly configured external directory links and relative installer links remain usable from a non-Git home directory, while indirect CWD and Git-worktree crossings remain blocked. Relative PATH entries now produce rejections only for executable files and retain their original spelling in diagnostics. Windows lookup expands PATHEXT once, with separate handling for Python 3.11's absolute-command behavior. Added relative-link acceptance/rejection fixtures, a portable regression for Windows metadata around .., and native Windows competing-suffix tests that use real shutil.which() calls. Focused local validation passed: 625 passed, 82 skipped; native Windows CI for the follow-up commit is pending. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous mode), which authored the implementation, tests, and this summary and ran validation.

Use followed directory metadata for reserved parent components instead of interpreting them as symlink entries. This prevents Windows from rebasing a relative directory link at the wrong parent while preserving intermediate trust checks. Add a portable regression for the Windows lstat/readlink behavior.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unnormalized relative link targets on Windows can resolve against the wrong parent, rejecting legitimate installations and blocking detection and upgrades.

2 open issues
3 resolved since last review

Review effort: Max (auto)

Comment thread src/specify_cli/_version.py Resolved
Copilot AI auto review requested due to automatic review settings October 30, 2026 12:46
Construct the directory symlink target with os.path.relpath so Windows receives its native path syntax. Verify the fixture resolves to the dispatcher before simulating parent-component metadata.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

On Windows, installer discovery under Python 3.12+ can skip safe PATHEXT candidates after rejecting an unsafe match, and a symlink test uses non-native separators that make the link untraversable.

3 open issues

Review effort: Max (auto)

Copilot AI auto review requested due to automatic review settings October 30, 2026 13:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because installer lookup tests still depend on the host’s real PATH, making failures and warnings environment-dependent.

2 open issues
1 resolved since last review

Review effort: Max (auto)

Give installer tests a per-test owned PATH and deterministic PATHEXT/current-directory lookup settings. Point installer fixtures at their own binary directory and add fixture-contract checks. Verify the same tests under empty and malformed inherited PATH values without changing production path handling.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 30, 2026 16:12
@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the host-PATH dependency in 995075c. Installer tests now start with a test-owned PATH and deterministic Windows lookup settings; installer fixtures explicitly select their own binary directory. Added checks for both fixture contracts. The five affected modules passed with an empty inherited PATH and with malformed inherited PATH entries: 408 passed, 29 skipped in each environment. The broader focused suite passed: 627 passed, 82 skipped. No production lookup behavior changed. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous mode), which authored the test changes and this summary and ran validation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because home-level dotfiles repositories can be mistaken for project roots, incorrectly blocking supported uv/pipx self-upgrades.

1 open issue
1 resolved since last review

Review effort: Max (auto)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants