From 18351b8c5ec8ad8e635a39f74179d2198390d70b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 13:19:04 +0000 Subject: [PATCH 1/3] fix(download_file): sanitize header-derived filename with basename Agent-Logs-Url: https://github.com/fabiocaccamo/python-fsutil/sessions/32602292-a86d-4d8e-8262-dae539be404a Co-authored-by: fabiocaccamo <1035294+fabiocaccamo@users.noreply.github.com> --- src/fsutil/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fsutil/operations.py b/src/fsutil/operations.py index 0f0b50a..156693c 100644 --- a/src/fsutil/operations.py +++ b/src/fsutil/operations.py @@ -213,7 +213,7 @@ def download_file( filename_pattern = r'filename="(.*)"' filename_match = re.search(filename_pattern, content_disposition) if filename_match: - filename = filename_match.group(1) + filename = os.path.basename(filename_match.group(1)) # or detect filename from url if not filename: filename = get_filename(url) From 07741bc430f1f02fdcbf9183fe364bd022dc5550 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:33:57 +0000 Subject: [PATCH 2/3] docs(download_file): annotate filename sanitization purpose Agent-Logs-Url: https://github.com/fabiocaccamo/python-fsutil/sessions/79e7623b-5e54-484b-838b-9b58f5877733 Co-authored-by: fabiocaccamo <1035294+fabiocaccamo@users.noreply.github.com> --- src/fsutil/operations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fsutil/operations.py b/src/fsutil/operations.py index 156693c..041e85a 100644 --- a/src/fsutil/operations.py +++ b/src/fsutil/operations.py @@ -213,6 +213,7 @@ def download_file( filename_pattern = r'filename="(.*)"' filename_match = re.search(filename_pattern, content_disposition) if filename_match: + # sanitize Content-Disposition filename to prevent path traversal filename = os.path.basename(filename_match.group(1)) # or detect filename from url if not filename: From a47e9c7c2aff733608be179d428c465b0a02eae8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:39:47 +0000 Subject: [PATCH 3/3] fix(download_file): sanitize windows separators and add regression test Agent-Logs-Url: https://github.com/fabiocaccamo/python-fsutil/sessions/6af6fe4f-2ec3-4e2e-91e3-8ec6fa5cbe5f Co-authored-by: fabiocaccamo <1035294+fabiocaccamo@users.noreply.github.com> --- src/fsutil/operations.py | 3 ++- tests/test_operations.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/fsutil/operations.py b/src/fsutil/operations.py index 041e85a..7d4e1d4 100644 --- a/src/fsutil/operations.py +++ b/src/fsutil/operations.py @@ -214,7 +214,8 @@ def download_file( filename_match = re.search(filename_pattern, content_disposition) if filename_match: # sanitize Content-Disposition filename to prevent path traversal - filename = os.path.basename(filename_match.group(1)) + filename = filename_match.group(1).replace("\\", "/") + filename = os.path.basename(filename) # or detect filename from url if not filename: filename = get_filename(url) diff --git a/tests/test_operations.py b/tests/test_operations.py index 4a84cba..ee8051e 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -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}"))