SEC2-022: Uploaded photographs are kept and forwarded with their embedded metadata, unlike every other picture Postulo stores #964

Closed
opened 2026-10-17 10:38:31 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo at 56fc9d531, checked by a second, independent pass.

Severity: Low · Category: Data Exposure · Reference: CWE-212 (Improper Removal of Sensitive Information Before Storage or Transfer); OWASP A01:2021 (Broken Access Control)

Location:

  • src/postulo/documents/forms.py:536-540 (help text "Postulo keeps it exactly as uploaded") and :695-709 (clean_file)
  • src/postulo/documents/integrity.py:95-104 (_picture_problem: decodes to check, keeps the bytes)
  • src/postulo/documents/views.py:1033-1070 (email_then_freeze: uploads attached byte-for-byte), src/postulo/documents/archiving.py (copies to stores)

Issue

core/pictures.as_stored re-encodes every avatar, logo and CV photo, and says why: "Re-encoding is the point rather than a side effect: it is what removes the metadata a phone's picture carries -- where it was taken, and when" (pictures.py:199-206). The uploaded-document path accepts png, jpg and jpeg (integrity.EXTENSIONS), decodes the picture only to prove it is one (_picture_problem), and stores the original bytes unchanged; the form promises as much. Those files are then attached to mail the person sends to an employer (email_then_freeze reads upload.file and attaches it as is), copied to every connected store, and included in the account archive.

A scanned diploma or certificate uploaded today is very often a phone photograph, and a phone photograph carries GPS coordinates, the capture time and the device model in EXIF. Nothing in this path strips, flags or mentions it; the threat model's sharpest asset is the home address, and a photograph taken at home carries it precisely.

Uploaded PDF, DOCX and ODT files likewise carry author and producer metadata and are forwarded the same way; that is what the help text promises, and it is recorded here for context rather than as part of this issue.

Impact

The account holder leaks their location and device to every employer and store they send a photographed document to, without being told. No cross-account effect.

Fix

Do not re-encode: a certificate re-encoded as JPEG loses quality, and the "file you gave it" promise, the stored SHA-256 (#663) and the never-edited rule (#217) all argue for keeping the pixels byte-identical. Instead:

  • add a checked-by-default "Remove the photo's metadata (location, time, camera)" box on UploadedDocumentForm that strips metadata losslessly in clean_file before the file is stored: for JPEG, drop the APP1 (Exif/XMP) and APP13 (IPTC) segments by walking the markers, keeping APP2 (ICC colour profile); for PNG, drop the eXIf, tEXt, iTXt and zTXt chunks;
  • change the help text to say what is kept and what is removed;
  • if the box is refused, the minimum is a warning when a GPS IFD (Image.getexif(), tag 0x8825) is present.

Done when a test uploads a JPEG carrying a GPS IFD and a PNG carrying an eXIf chunk and asserts the stored bytes decode to identical pixels and Image.getexif() is empty.

#878 (open) - metadata of exported PDF/DOCX/ODT, not of uploaded files; a different surface. #482 and #264 (closed) - re-encoding of avatars and logos, the rule this path does not follow. #663 and #217 (closed) - the stored checksum and the never-edited rule the fix must keep.

Found on 2026-10-16 in a read-only security review of `postulo` at `56fc9d531`, checked by a second, independent pass. **Severity:** Low · **Category:** Data Exposure · **Reference:** CWE-212 (Improper Removal of Sensitive Information Before Storage or Transfer); OWASP A01:2021 (Broken Access Control) **Location:** - `src/postulo/documents/forms.py:536-540` (help text "Postulo keeps it exactly as uploaded") and `:695-709` (`clean_file`) - `src/postulo/documents/integrity.py:95-104` (`_picture_problem`: decodes to check, keeps the bytes) - `src/postulo/documents/views.py:1033-1070` (`email_then_freeze`: uploads attached byte-for-byte), `src/postulo/documents/archiving.py` (copies to stores) ## Issue `core/pictures.as_stored` re-encodes every avatar, logo and CV photo, and says why: "Re-encoding is the point rather than a side effect: it is what removes the metadata a phone's picture carries -- where it was taken, and when" (`pictures.py:199-206`). The uploaded-document path accepts `png`, `jpg` and `jpeg` (`integrity.EXTENSIONS`), decodes the picture only to prove it is one (`_picture_problem`), and stores the original bytes unchanged; the form promises as much. Those files are then attached to mail the person sends to an employer (`email_then_freeze` reads `upload.file` and attaches it as is), copied to every connected store, and included in the account archive. A scanned diploma or certificate uploaded today is very often a phone photograph, and a phone photograph carries GPS coordinates, the capture time and the device model in EXIF. Nothing in this path strips, flags or mentions it; the threat model's sharpest asset is the home address, and a photograph taken at home carries it precisely. Uploaded PDF, DOCX and ODT files likewise carry author and producer metadata and are forwarded the same way; that is what the help text promises, and it is recorded here for context rather than as part of this issue. ## Impact The account holder leaks their location and device to every employer and store they send a photographed document to, without being told. No cross-account effect. ## Fix Do not re-encode: a certificate re-encoded as JPEG loses quality, and the "file you gave it" promise, the stored SHA-256 (#663) and the never-edited rule (#217) all argue for keeping the pixels byte-identical. Instead: - add a checked-by-default "Remove the photo's metadata (location, time, camera)" box on `UploadedDocumentForm` that strips metadata **losslessly** in `clean_file` before the file is stored: for JPEG, drop the APP1 (Exif/XMP) and APP13 (IPTC) segments by walking the markers, keeping APP2 (ICC colour profile); for PNG, drop the `eXIf`, `tEXt`, `iTXt` and `zTXt` chunks; - change the help text to say what is kept and what is removed; - if the box is refused, the minimum is a warning when a GPS IFD (`Image.getexif()`, tag 0x8825) is present. Done when a test uploads a JPEG carrying a GPS IFD and a PNG carrying an `eXIf` chunk and asserts the stored bytes decode to identical pixels and `Image.getexif()` is empty. ## Related #878 (open) - metadata of *exported* PDF/DOCX/ODT, not of uploaded files; a different surface. #482 and #264 (closed) - re-encoding of avatars and logos, the rule this path does not follow. #663 and #217 (closed) - the stored checksum and the never-edited rule the fix must keep.
tiagoagueda added this to the 0.6.0 milestone 2026-10-17 10:38:32 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#964
No description provided.