PST-048: Company-logo and plugin-logo decoding accepts every Pillow format from bytes a hostile website chooses #777
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#777
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 in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-048. Severity: Low.The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.
src/postulo/core/pictures.py(pictures.py:93-130)Who can do it, and what they need. The operator of a website that a member captures (the Find logo action fetches up to six images from the company's site through the guarded client), or a member uploading a logo.
How it works.
Bytes reach pictures.as_stored(data) with kinds=None, so Image.open identifies whatever Pillow's ~40 plugins recognise and image.load() decodes it. Verified: logos.process() accepts and re-encodes BMP, TIFF, ICO, GIF, WEBP, PPM, TGA, PCX, SGI, DDS, QOI, JPEG 2000 and ICNS. An EPS header is identified (format 'EPS'); it is refused only because no Ghostscript binary is on PATH here (and none is in the Docker image), so on a bare-metal install that has gs, Pillow's EPS loader would run Ghostscript on attacker-supplied PostScript.
Impact. A wider decoder attack surface (libtiff, openjpeg, libwebp and Pillow's own parsers, plus Ghostscript where installed) than the PNG/JPEG logos actually need. No concrete vulnerability is shown. Side effect: decode() sets Image.MAX_IMAGE_PIXELS process-wide on every call and never restores it.
Evidence.
Verification (verdict: confirmed; severity after verification: Low; first-pass claim: Informational). Attacker: the operator of a website that a member captures (Find logo fetches up to six images through the guarded client), or a member uploading a logo, which gives only self-attack. Precondition for real impact: Ghostscript on PATH on a bare-metal install, plus a -dSAFER bypass or an unpatched Ghostscript. Without Ghostscript it is informational. Suggested fix: pass a formats allowlist to Image.open (for example PNG, JPEG, GIF, WEBP, ICO, plus BMP if wanted), or give logos the same kinds= enforcement as avatars. Also stop assigning Image.MAX_IMAGE_PIXELS globally.
Suggested remediation. Pass formats=("PNG","JPEG","GIF","WEBP","ICO") to Image.open (or kinds=) on the logo paths, keep EPS and the exotic formats out for good, and restore Image.MAX_IMAGE_PIXELS after use.
Fixed in
c82645ad7on main.