fix: reject path traversal in dataset names on load/delete/exists#100
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTZQT3bQzqTBXQXvDsk4w9
sorenwacker
force-pushed
the
fix-dataset-name-traversal
branch
from
July 17, 2026 14:59
036926f to
836698c
Compare
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.
Security fix — path traversal via dataset name
FilesystemDatasetRepositoryinterpolates the dataset name straight into a filename (filesystem_dataset.py_get_path:self._dir / f"{name}.json"). The allowlistvalidate_dataset_name(^[a-zA-Z0-9][a-zA-Z0-9_-]*$) was applied only insave()—load(),delete(), andexists()skipped it. So a name like../secretor an absolute path escaped the datasets directory.Reachable unauthenticated over HTTP
POST /api/datasets/loadtakes the name viaBody(..., embed=True)(ui/routes/api.py), so/and..arrive unencoded, and the web app has no authentication.manager.load_dataset(name)→FilesystemDatasetRepository.load(name)→_get_path.Confirmed with a contained PoC (before the fix)
Targets must end in
.json(the suffix is appended), so this is arbitrary-.jsonread/delete rather than fully arbitrary — still a real information-disclosure and destructive primitive for a self-hosted instance.The fix
Move the existing name validation into
_get_path— the single choke point shared bysave/load/delete/exists— so every operation is protected, not just save.list()usesgloband is unaffected. Theloadroute already mapsValueError→ HTTP 400, so invalid names now return 400 instead of escaping.After the fix, all four PoC operations raise
ValueErrorand valid names (my_study-01) still save/load/delete/exist normally.Tests
Added to
TestFilesystemDatasetRepository:test_load_rejects_path_traversal— parametrized over../secret,../../etc/passwd,sub/../../escape,/etc/passwd,..test_delete_rejects_path_traversal— asserts an outsidevictim.jsonis not unlinkedtest_exists_rejects_path_traversaltests/test_repositories+test_ui/test_datasets.py+test_ui/test_dataset_manager.py: 88 passed. ruff + strict mypy clean.Not included (follow-ups)
repositories/file.pyFileEntityRepository.from_dataset_namehas the identicalbase_dir / f"{name}.json"pattern. It currently has no callers insrc/, so it is not reachable — but it should get the same guard before it is ever wired to input.🤖 Generated with Claude Code
https://claude.ai/code/session_01FTZQT3bQzqTBXQXvDsk4w9