Skip to content

Reject option-like VCS arguments a separator can't guard - #552

Merged
tony merged 5 commits into
masterfrom
command-consistency-fix
Aug 30, 2026
Merged

Reject option-like VCS arguments a separator can't guard#552
tony merged 5 commits into
masterfrom
command-consistency-fix

Conversation

@tony

@tony tony commented Aug 30, 2026

Copy link
Copy Markdown
Member

Summary

  • FixGit.pull() executed an arbitrary command when repository/reftag began with -: git pull re-invokes git fetch without an end-of-options --, so --upload-pack=<cmd> reached the child fetch as an option. It now raises LibVCSException.
  • FixGitSync(rev=...) truncated an arbitrary file when the configured revision began with -: the value reached git rev-list <commit>, where --output=<file> is a diff option that opens the file for writing during option parsing. update_repo() now records a rev error instead of running the command.
  • HardenSvn.checkout(url=...) rejects a URL beginning with -; svn's --config-option can set a tunnel command. This matches the protection the git and hg clone paths already carry.
  • Add — a regression test pinning the -- separator ahead of the git clone URL, the one git-side separator no test covered.
  • Add — a shared reject_option_like() guard in _internal/run.py, applied where an end-of-options -- cannot neutralize an option-like value.

Design decisions

  • Reject, don't insert --, where the separator can't protectgit pull re-spawns git fetch with no separator, a rev after -- is read as a pathspec (git 2.44+ needed for --end-of-options), and svn's run() appends global options after the subcommand args so a -- would swallow them. A leading-- guard is the only fix that holds in all three; the working -- separators on clone/fetch/hg are left untouched.
  • Guard at the layer where the untrusted value enters — the pull vector is reachable only through the public Git.pull() API, so the guard sits in cmd/; the rev vector enters via config through GitSync, so that guard sits in sync/ and surfaces as a SyncResult error rather than an exception.

Test plan

  • uv run pytest — full suite plus doctests (doctest flags live in pyproject.toml)
  • uv run ruff format . --check and uv run ruff check .
  • uv run mypy .
  • test_pull_rejects_option_like_repository — drives the injection through the real Git.pull() API and asserts no command runs
  • test_update_repo_rejects_option_like_rev — asserts a victim file is left intact
  • test_checkout_rejects_option_like_url — rejects an option-like svn URL
  • test_clone_places_end_of_options_before_url — pins -- before the clone URL (proven to fail if the separator is dropped)

Verification

rg -n 'reject_option_like' src/libvcs
rg -n '"clone", \*local_flags, "--"' src/libvcs/cmd/git.py

tony added 5 commits August 30, 2026 08:00
git pull re-invokes git fetch without an end-of-options --, so the
separator libvcs places before pull's positionals never reached the
child fetch. A repository or reftag beginning with - was parsed there
as an option; --upload-pack=<cmd> ran an arbitrary command.

Reject a - prefix in both positions via a shared reject_option_like()
helper in _internal/run, raising LibVCSException before the command is
built. A regression test drives the injection through the real Git.pull
API and asserts the canary is never created.
A configured rev flowed into git rev-list <commit>, which places the
operand with no end-of-options --. A rev of --output=<file> was parsed
there as git's diff --output option, truncating the file during option
parsing -- file destruction from a config-supplied revision.

Validate rev with reject_option_like() before use and record a "rev"
SyncResult error instead of running the command. A regression test
drives a malicious rev through update_repo and asserts the victim file
is left intact.
svn checkout takes the URL as its first positional with no end-of-options
separator, so a URL beginning with - was parsed as an option; svn's
--config-option can set a tunnel command, an execution primitive. Reject
a - prefix via reject_option_like(), matching the protection the git and
hg clone paths already carry.

relocate() and switch() are unaffected: they convert a non-file:// target
through pathlib as_uri(), so the value can never reach argv option-like.
Assert git clone emits -- immediately before the URL. This was the only
git-side separator with no test; a refactor dropping it would else pass,
because the single-positional clone shape never runs the injected
--upload-pack helper, making an end-to-end canary test vacuous here
(unlike the hg alias path, whose regression test does execute).
Regenerated as a single CHANGES-only commit: the guards for Git.pull,
GitSync rev, and Svn.checkout, crediting the report that prompted the
audit. The entries were previously entangled in the three fix commits.
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.27273% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.50%. Comparing base (6ba039d) to head (1e40a0e).

Files with missing lines Patch % Lines
tests/cmd/test_svn.py 0.00% 5 Missing ⚠️
src/libvcs/cmd/svn.py 0.00% 2 Missing ⚠️
src/libvcs/_internal/run.py 80.00% 1 Missing ⚠️
src/libvcs/cmd/git.py 66.66% 1 Missing ⚠️
src/libvcs/sync/git.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #552      +/-   ##
==========================================
+ Coverage   61.34%   61.50%   +0.15%     
==========================================
  Files          40       40              
  Lines        6556     6595      +39     
  Branches     1104     1106       +2     
==========================================
+ Hits         4022     4056      +34     
- Misses       1940     1946       +6     
+ Partials      594      593       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony
tony merged commit e8c745d into master Aug 30, 2026
7 checks passed
@tony
tony deleted the command-consistency-fix branch August 30, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant