fix: contain model deletion/download paths against path traversal (#2267) - #2273
Open
DeathSurfing wants to merge 1 commit into
Open
fix: contain model deletion/download paths against path traversal (#2267)#2273DeathSurfing wants to merge 1 commit into
DeathSurfing wants to merge 1 commit into
Conversation
- validate_model_path_contained() rejects ids whose normalized path resolves outside (or to) a models root, mirroring the existing trace-path pattern - delete_model(), build_model_path(), ensure_cache_dir() validate before any rmtree/makedirs; DELETE /download rejects "."/".." with HTTP 400; the download coordinator survives invalid ids without losing its command loop - tests cover traversal rejection for the delete/download helpers, including the case where an existing-model lookup would return a traversal path Closes exo-explore#2267
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2267: a path traversal in the model deletion flow.
DELETE /download/{node_id}/{model_id}acceptsmodel_idvalues like..(delivered URL-encoded as%2e%2e), which reachdelete_model()andshutil.rmtree()a path derived from attacker input with no containment check.ModelId.normalize()only replaces/with--, so a bare..passes through unchanged andmodels_dir / ".."resolves to the parent of the model directory — on a default install, the whole exo data home (~/.local/share/exo), including downloaded weights, the event log, and custom model cards. A bare.likewise resolves to the models root itself.Changes
validate_model_path_contained()(download_utils.py): new helper that resolvesroot / model_id.normalize()and raisesValueErrorif it escapes the root or resolves to the root itself (./ empty-id protection). Mirrors the containment pattern already used for trace files (Path.resolve().is_relative_to(...)).delete_model(): validates against each writable models dir and the cache root before anyrmtree.build_model_path(): validates before the existing-model lookup, so a traversal-resolved path (e.g. the parent dir, which can look like a complete model) can never be returned or used as a download target.ensure_cache_dir(): validates beforemakedirs, so no stray directory is created outside the cache root.DELETE /download(api/main.py): rejects ids whose normalized form is./..withHTTP 400before a command is sent.DownloadCoordinator._delete_download: catchesValueError/OSErrorfromdelete_modeland logs, so one bad id cannot kill the command-processing loop.Verification
uv run pytest src/exo/download/tests/→ 77 passed (10 new tests: traversal/dot-id rejection for delete and download-side helpers, legit-id positive controls, and the regression where the existing-model lookup would return a traversal path).uv run ruff checkanduv run ruff format --check→ clean on all changed files (treefmt uses ruff-format for Python).uv run basedpyright→ no errors/warnings in any changed file.delete_model()/build_model_path()/ensure_cache_dir()against a sandboxed tree:..and.raiseValueError, the victim parent directory survives, no stray dirs are created, and legitimate ids still work.Test plan
pytestsuite above.curl -X DELETE http://<node>:52415/download/<node_id>/%2e%2e→400 Invalid model id: ..; deleting a real downloaded model still works.Closes #2267