Image compression in an E2EE room uploads plaintext and breaks the event #6

Closed
opened 2026-09-12 01:50:46 -04:00 by jared · 0 comments
Owner

Severity: critical · Type: security · Confidence: high

Location: src/app/features/room/RoomInput.tsx:468-513 (compression branch of handleSendUpload), src/app/features/room/msgContent.ts:68-74, src/app/components/upload-card/UploadCardRenderer.tsx:118-167

Problem

handleFiles encrypts every attachment when room.hasEncryptionStateEvent(), producing {file: encryptedBlob, originalFile: plaintext, encInfo}. The opt-in "compress image" path in handleSendUpload then re-compresses fileItem.originalFile (the plaintext) and uploads it with a bare mx.uploadContent(compressedFile, …) — no encryptFile call — so the decrypted image is stored unencrypted on the media repo. Worse, the synthetic compressedItem is built with {...fileItem, file, originalFile}, which keeps the original file's encInfo, so getImageMsgContent emits content.file = {...originalEncInfo, url: compressedMxc}. Receivers (and the sender) will try to AES-decrypt a plaintext JPEG with the wrong key/IV/SHA-256 and the attachment fails to render. The encrypted original is then deleted via tryDeleteMxcContent, so there is no fallback copy.

How to trigger

In an encrypted room, attach a JPEG/WebP (non-PNG raster), tick "Compress image", send. The media is on the server in the clear and the message renders as a broken/undecryptable image.

Suggested fix

In the compression branch, when fileItem.encInfo is present run encryptFile(compressedFile) and upload the encrypted blob, then build compressedItem with the new encInfo; otherwise strip encInfo from compressedItem. Add a regression test asserting encInfo matches the uploaded bytes.


Filed from the September 2026 client audit (branch lotus @ 4bea4895).

**Severity:** critical · **Type:** security · **Confidence:** high **Location:** `src/app/features/room/RoomInput.tsx:468-513` (compression branch of `handleSendUpload`), `src/app/features/room/msgContent.ts:68-74`, `src/app/components/upload-card/UploadCardRenderer.tsx:118-167` ### Problem `handleFiles` encrypts every attachment when `room.hasEncryptionStateEvent()`, producing `{file: encryptedBlob, originalFile: plaintext, encInfo}`. The opt-in "compress image" path in `handleSendUpload` then re-compresses `fileItem.originalFile` (the **plaintext**) and uploads it with a bare `mx.uploadContent(compressedFile, …)` — no `encryptFile` call — so the decrypted image is stored unencrypted on the media repo. Worse, the synthetic `compressedItem` is built with `{...fileItem, file, originalFile}`, which **keeps the original file's `encInfo`**, so `getImageMsgContent` emits `content.file = {...originalEncInfo, url: compressedMxc}`. Receivers (and the sender) will try to AES-decrypt a plaintext JPEG with the wrong key/IV/SHA-256 and the attachment fails to render. The encrypted original is then deleted via `tryDeleteMxcContent`, so there is no fallback copy. ### How to trigger In an encrypted room, attach a JPEG/WebP (non-PNG raster), tick "Compress image", send. The media is on the server in the clear and the message renders as a broken/undecryptable image. ### Suggested fix In the compression branch, when `fileItem.encInfo` is present run `encryptFile(compressedFile)` and upload the encrypted blob, then build `compressedItem` with the **new** `encInfo`; otherwise strip `encInfo` from `compressedItem`. Add a regression test asserting `encInfo` matches the uploaded bytes. --- _Filed from the September 2026 client audit (branch `lotus` @ 4bea4895)._
jared added this to the Audit 2026-09 · P0 security & data loss milestone 2026-09-12 01:50:46 -04:00
jared added the bugpriority: criticalsecurityarea: messaging labels 2026-09-12 01:50:46 -04:00
jared self-assigned this 2026-09-12 01:50:46 -04:00
jared closed this issue 2026-09-12 02:14:51 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
2026-09-18
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#6