From 33a91f33299a05ade7aa54d0d6c9f194d40f107b Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 9 Sep 2025 14:25:53 +0000 Subject: [PATCH 1/3] Handle too-long metadata filenames --- tests/unit/forklift/test_legacy.py | 85 ++++++++++++++++++++++++++++++ warehouse/forklift/legacy.py | 18 ++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py index 2a81315a25eb..4416345fb206 100644 --- a/tests/unit/forklift/test_legacy.py +++ b/tests/unit/forklift/test_legacy.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 import base64 +import builtins import hashlib import io import json @@ -2031,6 +2032,90 @@ def test_upload_succeeds_custom_project_size_limit( ("example", "1.0", "add source file example-1.0.tar.gz", user), ] + def test_upload_fails_with_oserror_on_metadata_write( + self, tmpdir, monkeypatch, pyramid_config, db_request + ): + monkeypatch.setattr(tempfile, "tempdir", str(tmpdir)) + monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: (True, None)) + + user = UserFactory.create() + EmailFactory.create(user=user) + project = ProjectFactory.create() + release = ReleaseFactory.create(project=project, version="1.0") + RoleFactory.create(user=user, project=project) + + filename = f"{project.normalized_name.replace('-', '_')}-{release.version}-py2.py3-none-any.whl" + + db_request.user = user + pyramid_config.testing_securitypolicy(identity=user) + + db_request.user_agent = "warehouse-tests/6.6.6" + + wheel_metadata = dedent( + """ + Metadata-Version: 2.1 + Name: {project.name} + Version: {release.version} + """ + ).encode("utf-8") + + wheel_testdata = _get_whl_testdata( + name=project.normalized_name.replace("-", "_"), version=release.version + ) + wheel_md5 = hashlib.md5(wheel_testdata).hexdigest() + + content = FieldStorage() + content.filename = filename + content.file = io.BytesIO(wheel_testdata) + content.type = "application/octet-stream" + + db_request.POST = MultiDict( + { + "metadata_version": "2.1", + "name": project.name, + "version": release.version, + "filetype": "bdist_wheel", + "pyversion": "py2.py3", + "content": content, + "md5_digest": wheel_md5, + "wheel_metadata_version": "1.0", + "wheel_metadata": base64.b64encode(wheel_metadata).decode("ascii"), + } + ) + + storage_service = pretend.stub(store=lambda path, file_path, *, meta: None) + db_request.find_service = pretend.call_recorder( + lambda svc, name=None, context=None: { + IFileStorage: storage_service, + }.get(svc) + ) + + # Patch open to raise OSError + original_open = builtins.open + + def mock_open(file, mode="r", *args, **kwargs): + if str(file).endswith(".metadata"): + raise OSError("Filename too long") + return original_open(file, mode, *args, **kwargs) + + monkeypatch.setattr("builtins.open", mock_open) + + with pytest.raises(HTTPBadRequest) as excinfo: + legacy.file_upload(db_request) + + resp = excinfo.value + + assert resp.status_code == 400 + assert resp.status == f"400 Filename is too long: '{filename}'" + + assert db_request.metrics.increment.calls == [ + pretend.call("warehouse.upload.attempt"), + pretend.call( + "warehouse.upload.failed", + tags=["reason:filename-too-long", "filetype:bdist_wheel"], + ), + ] + def test_upload_fails_with_previously_used_filename( self, pyramid_config, db_request ): diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py index 5422785f6322..4e2745c7a026 100644 --- a/warehouse/forklift/legacy.py +++ b/warehouse/forklift/legacy.py @@ -1536,8 +1536,22 @@ def file_upload(request): filename=filename, metadata_filename=metadata_filename ), ) - with open(temporary_filename + ".metadata", "wb") as fp: - fp.write(wheel_metadata_contents) + try: + with open(temporary_filename + ".metadata", "wb") as fp: + fp.write(wheel_metadata_contents) + except OSError: + request.metrics.increment( + "warehouse.upload.failed", + tags=[ + "reason:filename-too-long", + f"filetype:{form.filetype.data}", + ], + ) + raise _exc_with_message( + HTTPBadRequest, + f"Filename is too long: '{filename}'", + ) + metadata_file_hashes = { "sha256": hashlib.sha256(), "blake2_256": hashlib.blake2b(digest_size=256 // 8), From 339406785cf9c2daa88bc45fdcbd02818fd1b75d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 9 Sep 2025 14:33:10 +0000 Subject: [PATCH 2/3] Linting --- tests/unit/forklift/test_legacy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py index 4416345fb206..d3e96636709e 100644 --- a/tests/unit/forklift/test_legacy.py +++ b/tests/unit/forklift/test_legacy.py @@ -2036,7 +2036,9 @@ def test_upload_fails_with_oserror_on_metadata_write( self, tmpdir, monkeypatch, pyramid_config, db_request ): monkeypatch.setattr(tempfile, "tempdir", str(tmpdir)) - monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: (True, None)) + monkeypatch.setattr( + legacy, "_is_valid_dist_file", lambda *a, **kw: (True, None) + ) user = UserFactory.create() EmailFactory.create(user=user) From 73a9ec9bf827f3c14a65497782b9bf0abb3fe0b2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 9 Sep 2025 14:46:22 +0000 Subject: [PATCH 3/3] More linting --- tests/unit/forklift/test_legacy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py index d3e96636709e..8fd337e68a4a 100644 --- a/tests/unit/forklift/test_legacy.py +++ b/tests/unit/forklift/test_legacy.py @@ -2046,7 +2046,10 @@ def test_upload_fails_with_oserror_on_metadata_write( release = ReleaseFactory.create(project=project, version="1.0") RoleFactory.create(user=user, project=project) - filename = f"{project.normalized_name.replace('-', '_')}-{release.version}-py2.py3-none-any.whl" + filename = "{}-{}-py2.py3-none-any.whl".format( + project.normalized_name.replace("-", "_"), + release.version, + ) db_request.user = user pyramid_config.testing_securitypolicy(identity=user)