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
4 changes: 3 additions & 1 deletion src/fsutil/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ def download_file(
filename_pattern = r'filename="(.*)"'
filename_match = re.search(filename_pattern, content_disposition)
if filename_match:
filename = filename_match.group(1)
# sanitize Content-Disposition filename to prevent path traversal
filename = filename_match.group(1).replace("\\", "/")
filename = os.path.basename(filename)
# or detect filename from url
if not filename:
filename = get_filename(url)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,33 @@ def test_download_file_without_requests_installed(temp_path):
fsutil.download_file(url, dirpath=temp_path())


def test_download_file_content_disposition_filename_is_sanitized(temp_path):
class MockResponse:
headers = {"content-disposition": 'attachment; filename="..\\..\\evil.txt"'}

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
return None

def raise_for_status(self):
return None

def iter_content(self, chunk_size=1):
yield b"hello world"

class MockRequests:
def get(self, *args, **kwargs):
return MockResponse()

with patch("fsutil.operations.require_requests", return_value=MockRequests()):
path = fsutil.download_file("https://example.com/file.txt", dirpath=temp_path())
assert fsutil.exists(path)
assert fsutil.get_filename(path) == "evil.txt"
assert path == temp_path("evil.txt")


def test_list_dirs(temp_path):
for i in range(0, 5):
fsutil.create_dir(temp_path(f"a/b/c/d-{i}"))
Expand Down
Loading