|
43 | 43 | [ |
44 | 44 | ("global-pax", "global PAX.*cumulative size.*8192"), |
45 | 45 | ("metadata-chain", "metadata chain.*maximum depth.*32"), |
| 46 | + ("total-metadata", "archive metadata.*cumulative size.*1048576"), |
46 | 47 | ], |
47 | 48 | ) |
48 | 49 | def test_tar_archive_metadata_budgets_reject_before_parsing( |
@@ -86,6 +87,73 @@ def bounded_parser(member, archive): |
86 | 87 | assert not out_dir.exists() |
87 | 88 |
|
88 | 89 |
|
| 90 | +@pytest.mark.parametrize("metadata_type", [tarfile.XHDTYPE, tarfile.SOLARIS_XHDTYPE]) |
| 91 | +@pytest.mark.parametrize("over_budget", [False, True]) |
| 92 | +def test_per_member_metadata_cumulative_boundary( |
| 93 | + tmp_path, monkeypatch, metadata_type, over_budget |
| 94 | +): |
| 95 | + plain = b"" |
| 96 | + for index, size in enumerate([65536] * 15 + [65535]): |
| 97 | + plain += tar_metadata_record(metadata_type, size) |
| 98 | + plain += tarfile.TarInfo(f"file{index}.txt").tobuf() |
| 99 | + plain += tar_metadata_record(tarfile.GNUTYPE_LONGLINK, 2 if over_budget else 1) |
| 100 | + plain += tarfile.TarInfo("last.txt").tobuf() |
| 101 | + archive_path = tmp_path / "per-member.tar.gz" |
| 102 | + archive_path.write_bytes(gzip.compress(plain + b"\0" * tarfile.BLOCKSIZE)) |
| 103 | + original_parser = tarfile.TarInfo._proc_member |
| 104 | + parsed_bytes = {} |
| 105 | + |
| 106 | + def bounded_parser(member, archive): |
| 107 | + if member.type in ( |
| 108 | + tarfile.XHDTYPE, tarfile.SOLARIS_XHDTYPE, tarfile.GNUTYPE_LONGLINK |
| 109 | + ): |
| 110 | + parsed_bytes[archive] = parsed_bytes.get(archive, 0) + member.size |
| 111 | + assert parsed_bytes[archive] <= 1048576, "Excess metadata reached parser" |
| 112 | + return original_parser(member, archive) |
| 113 | + |
| 114 | + monkeypatch.setattr(tarfile.TarInfo, "_proc_member", bounded_parser) |
| 115 | + if over_budget: |
| 116 | + with pytest.raises(_CustomZipError, match="archive metadata.*cumulative size.*1048576"): |
| 117 | + safe_extract_archive( |
| 118 | + archive_path, tmp_path / "out", error_type=_CustomZipError |
| 119 | + ) |
| 120 | + assert not (tmp_path / "out").exists() |
| 121 | + else: |
| 122 | + for index in range(2): |
| 123 | + out_dir = tmp_path / f"out{index}" |
| 124 | + assert safe_extract_archive(archive_path, out_dir) == "tar.gz" |
| 125 | + assert len(list(out_dir.iterdir())) == 17 |
| 126 | + |
| 127 | + |
| 128 | +@pytest.mark.parametrize( |
| 129 | + "metadata_type", |
| 130 | + [tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.SOLARIS_XHDTYPE, |
| 131 | + tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK], |
| 132 | +) |
| 133 | +@pytest.mark.parametrize("over_budget", [False, True]) |
| 134 | +def test_cumulative_metadata_budget_includes_every_header_type( |
| 135 | + tmp_path, monkeypatch, metadata_type, over_budget |
| 136 | +): |
| 137 | + monkeypatch.setattr( |
| 138 | + _download_security, "MAX_TAR_TOTAL_METADATA_BYTES", 64, raising=False |
| 139 | + ) |
| 140 | + plain = tar_metadata_record(tarfile.XGLTYPE, 32) |
| 141 | + plain += tarfile.TarInfo("first.txt").tobuf() |
| 142 | + plain += tar_metadata_record(metadata_type, 33 if over_budget else 32) |
| 143 | + plain += tarfile.TarInfo("last.txt").tobuf() |
| 144 | + archive_path = tmp_path / "mixed.tar.gz" |
| 145 | + archive_path.write_bytes(gzip.compress(plain + b"\0" * tarfile.BLOCKSIZE)) |
| 146 | + if over_budget: |
| 147 | + with pytest.raises(_CustomZipError, match="archive metadata.*cumulative size.*64"): |
| 148 | + safe_extract_archive( |
| 149 | + archive_path, tmp_path / "out", error_type=_CustomZipError |
| 150 | + ) |
| 151 | + assert not (tmp_path / "out").exists() |
| 152 | + else: |
| 153 | + assert safe_extract_archive(archive_path, tmp_path / "out") == "tar.gz" |
| 154 | + assert len(list((tmp_path / "out").iterdir())) == 2 |
| 155 | + |
| 156 | + |
89 | 157 | @pytest.mark.parametrize("extractor", [safe_extract_tar, safe_extract_archive]) |
90 | 158 | @pytest.mark.parametrize("over_budget", [False, True]) |
91 | 159 | def test_interleaved_global_pax_budget_is_cumulative( |
@@ -368,7 +436,7 @@ def test_tiny_compressed_pax_archive_rejects_large_metadata(tmp_path): |
368 | 436 |
|
369 | 437 | def test_tar_metadata_limit_does_not_limit_regular_file_payload(tmp_path): |
370 | 438 | archive_path = tmp_path / "payload.tar.gz" |
371 | | - contents = b"x" * 65537 |
| 439 | + contents = b"x" * 1048577 |
372 | 440 | _write_tar_gz(archive_path, [("file.txt", contents)]) |
373 | 441 | out_dir = tmp_path / "out" |
374 | 442 |
|
|
0 commit comments