SEC2-027: read_wheel reads a wheel's metadata members without a size cap #969
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#969
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Informational · Category: Input Validation · Reference: CWE-409 (Improper Handling of Highly Compressed Data); CWE-400 (Uncontrolled Resource Consumption); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo/plugins/installing.py:212-245(wheel.read(metadata_name),wheel.read(wheel_name),wheel.read(points_name))src/postulo/core/server_views.py:1776-1797(_upload: the wheel is written to disk fromupload.chunks()and handed toread_wheelwith no check of its own)src/postulo/core/middleware.py:295-296(the request ceiling for/server/plugins/actionismax(POSTULO_UPLOAD_CEILING_MB, 64)MB, compressed)Issue
The other zip readers in the core ask the archive's directory first and read a bounded number of bytes (
documents/readers.py:60-66,core/importer.py:_read_within).read_wheeldoes not:A
.dist-info/METADATA,WHEELorentry_points.txtmember that inflates to gigabytes is read whole into the worker's memory, beforecheck()can refuse anything. The upload path admits up to 64 MB compressed (deflate inflates about a thousandfold), and the catalogue path up toMAX_WHEEL_BYTES(40 MB) after the checksum matched;namelist()is likewise unbounded in count.Impact
Only an administrator reaches the page, and a catalogue wheel has to carry the published checksum, so this is a self-inflicted or catalogue-publisher denial of service: one worker is killed by the OOM killer or held for minutes while a wheel the administrator meant to inspect is read. Informational, as defence in depth.
Fix
Bound each member before reading it, as
readers.text_ofdoes:for the three members; refuse an encrypted member (
info.flag_bits & 0x1) asreaders.pydoes; and refuse a wheel whosenamelist()exceeds a few thousand entries. Done when:tests/test_plugin_install.pybuilds a wheel whoseMETADATAdeclares a 2 GBfile_size(a stored entry with a lying header, or a deflate bomb) and assertsread_wheelraisesInstallErrorwithout reading it.Related
#466 (closed) - the same class of caps for the account-archive import; nothing covers the wheel reader.