Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/huggingface_hub/_commit_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,19 @@ def _validate_path_in_repo(path_in_repo: str) -> str:
raise ValueError(f"Invalid `path_in_repo` in CommitOperation: '{path_in_repo}'")
if path_in_repo.startswith("./"):
path_in_repo = path_in_repo[2:]

# The checks above only catch a ".." segment at the very start of the path. A
# path like "a/../../etc/passwd" doesn't start with "../" but still escapes
# the repo root once its ".." segments are resolved, so walk the full path and
# track depth relative to the repo root to catch that case too.
depth = 0
for part in path_in_repo.split("/"):
if part in ("", "."):
continue
depth += -1 if part == ".." else 1
if depth < 0:
raise ValueError(f"Invalid `path_in_repo` in CommitOperation: '{path_in_repo}'")

for forbidden in FORBIDDEN_FOLDERS:
if any(part == forbidden for part in path_in_repo.split("/")):
raise ValueError(
Expand Down
13 changes: 12 additions & 1 deletion tests/test_commit_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ class TestCommitOperationPathInRepo:
".file.txt": ".file.txt",
"/file.txt": "file.txt",
"./file.txt": "file.txt",
# ".." segments that resolve back into the repo (never escape the root) are fine
"a/../file.txt": "a/../file.txt",
"a/b/../../c/file.txt": "a/b/../../c/file.txt",
}
invalid_values = [".", "..", "../file.txt"]
invalid_values = [
".",
"..",
"../file.txt",
# a ".." segment anywhere in the path can still escape the repo root, not just
# a leading "../"
"a/../../file.txt",
"a/b/../../../file.txt",
]

def test_path_in_repo_valid(self) -> None:
for input, expected in self.valid_values.items():
Expand Down