Repository navigation
checkout: refuse outputs that resolve outside the repository - #11105
ompatil1906 wants to merge 1 commit into
Conversation
Fixes treeverse#11101 Signed-off-by: Om Patil <patilom1906@gmail.com>
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #11105 +/- ##
==========================================
+ Coverage 90.68% 90.99% +0.31%
==========================================
Files 504 505 +1
Lines 39795 41223 +1428
Branches 3141 3270 +129
==========================================
+ Hits 36087 37511 +1424
- Misses 3042 3072 +30
+ Partials 666 640 -26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Heads-up on the failing |
Summary
dvc checkout/dvc pullcan materialize a tracked output through anintermediate symlink, allowing arbitrary file writes outside the clone root
and installation of executable Git hooks (code execution on the next
git commitin CI that pulls an untrusted repository).Public issue: #11101
Problem
Output.is_in_repo/fs.isincontainment is performed on the declaredlexical path (
commonpath), while the OS resolves symlinks at write timeinside
dvc_data.index.checkout.apply. A repo that containsoutlink -> ../outsidewith an output declared asoutlink/payload.bin, orhookdir -> .git/hookswithhookdir/pre-commit, can - with perfectly validGit tree, DVC descriptor and remote object - cause the fetched object to land
outside the project root or as an executable hook under
.git/hooks.Root cause
The containment check never establishes that the canonical filesystem
destination stays inside the project. The temporary-file creation and
os.replaceperformed bydvc-objectsfollow the intermediate symlinks, andno physical-root or protected-
.gitcheck exists at the materializationboundary.
Fix
Add a canonical containment gate at the checkout boundary in
dvc/repo/checkout.py:realpathboth sides (output destination and repo root /.git).root/.gitwith a loud, clearCheckoutError- never a silent skip,independent of
--force.dvc pullanddvc checkout, which converge here.Behavior change note: outputs whose canonical destination resolves outside
the repository (or inside
.git) are now rejected with a clear error. Thisdeliberately breaks the unusual-but-legal setup "output path crosses a symlink
to outside" - loudly rather than silently.
Testing
tests/func/test_checkout.py:.git/hooksvia parent symlink;.git;pytest tests/func/test_checkout.py- 53 passed.ruff checkandruff format --checkclean.reproduce.sh) rerun: controlmaterializes; the escape and
.git/hooksattacks are blocked with the newerror messages and no artifact is written inside or outside; the
in-directory tree-entry bypass variant is also blocked (
dvc pullrc 1).Follow-ups (out of scope)
dvc run/reprorun-cache restore,sync/import) anddvc-data/dvc-objectsstill perform rawsymlink-following writes; consider moving the guard into
dvc_data.index.checkout.applyfor defense-in-depth, and evaluate asecurity advisory for the
.git/hookscode-execution chain.untrusted-repo CI scenario.
Fixes #11101
❗ I have followed the Contributing to DVC checklist.
📖 If this PR requires documentation updates, I have created a separate PR (or issue, at least) in dvc.org and linked it here.
Thank you for the contribution - we'll try to review it as soon as possible. 🙏