RocketChat / RocketChat/Rocket.Chat
E2EE file send: rooms.mediaConfirm is not idempotent; Chrome reconnect inserts N duplicate file messages
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 46.1k
- Forks
- 13.9k
- Avg merge
- 3d 3h
- Merged PRs (30d)
- 130
Description
Description:
On Rocket.Chat 8.6.1 with E2EE + Encrypt files enabled, sending a file in an encrypted DM uses the two-step API (POST /api/v1/rooms.media/:rid then POST /api/v1/rooms.mediaConfirm/:rid/:fileId).
In production we observed:
- The client successfully uploaded the encrypted blob (
rooms.mediaHTTP 200) twice. - It did not call
rooms.mediaConfirmat all during that session. The sender saw “nothing happened” / send failed. The first upload stayed temporary (expiresAt+24h). The second upload sat unconfirmed. - About 2 hours later, on Chrome reconnect, the client fired 18
rooms.mediaConfirmrequests for the samefileIdin ~300ms: 15× HTTP 200 and 3× HTTP 429. - The server created 15 distinct messages, all
t: "e2e", all pointing at the sameapplication/octet-streamwhose name is the SHA-256 of the original filename (by design from E2EE file encryption).msgwas empty and messagecontentwas missing; encryption metadata lived only onrocketchat_uploads.content. Recipients who downloaded the attachment outside the E2EE decrypt path got an unreadable hash-named blob of the same size as the original.doc.
rooms.mediaConfirm is not idempotent. Each successful confirm calls sendFileMessage again. There is no unique constraint on rocketchat_message.file._id (only a non-unique file._id_1 index).
This matches the long-standing E2EE upload flakiness (modal does nothing, file appears later under Files, reconnect flushes a queued confirm) plus a server-side multiplier.
Related: #32963, #33973, #34956. This is not the old MIME whitelist error-invalid-file-type case — rooms.media returned 200. FileUpload whitelist is empty; blacklist is only image/svg+xml.
Steps to reproduce:
- Enable E2EE, Encrypt files, and default encryption for DMs.
- Open an encrypted DM in Chrome.
- Send a file (our case: a
.docwith a non-ASCII filename, ~271 KB). - If the file-upload modal appears to do nothing, retry once (second
rooms.media). Do not leave the tab until after a reconnect / session drop. - Wait until the browser reconnects (in our case ~2 hours later, new RC session, same Chrome).
We did not need special MIME whitelist settings.
Expected behavior:
- One upload + one confirm → one message.
- A repeated
rooms.mediaConfirmfor an already confirmedfileIdshould return the existing message (idempotent), not insert another. - If
rooms.mediasucceeded but confirm did not, the client should retry once with backoff, not N parallel confirms on reconnect. - The recipient’s client should decrypt the file to the original name/type when E2EE keys for the room are present.
Actual behavior:
Nginx (UTC):
05:53:03POST /api/v1/rooms.media/{rid}→ 200 (upload A, left withexpiresAt)05:53:16POST /api/v1/rooms.media/{rid}→ 200 (upload B, same hash name, 277504 bytes)- no
mediaConfirmin that session 07:48:33-34POST /api/v1/rooms.mediaConfirm/{rid}/{fileId}→ 15x 200 + 3x 429
Mongo: 15 messages, 15 distinct _ids, one file._id, all t: "e2e", empty msg, no content. Upload content.algorithm was rc.v2.aes-sha2; kid matched room.e2eKeyId.
Server Setup Information:
- Version of Rocket.Chat Server: 8.6.1 (
rocketchat/rocket.chat:8.6.1;/api/inforeports8.6) - License Type: Enterprise
- Number of Users: ~50
- Operating System: Ubuntu Linux x86_64
- Deployment Method: Docker Compose, official image, nginx reverse proxy
- Number of Running Instances: 1
- DB Replicaset Oplog: Yes (
rs0) - NodeJS Version: 22.22.3 (image)
- MongoDB Version: 8.0.26
- File storage: GridFS
- E2EE:
E2E_Enable=true,E2E_Enable_Encrypt_Files=true,E2E_Enabled_Default_DirectRooms=true,E2E_Allow_Unencrypted_Messages=true
Client Setup Information
- Desktop App or Browser Version: Chrome 151, Windows 10 (web client, not Electron)
- Operating System: Windows 10
- Sender IP was a single residential client; many short Chrome sessions the same morning (reconnects)
Additional context
Suggested server fix (small):
- In
rooms.mediaConfirm, ifMessages.getMessageByFileIdAndUsername(fileId, userId)already returns a message, return it and do not callsendFileMessage. - Treat duplicate-key / already-confirmed races the same way (confirm can be concurrent).
- Unique index on
rocketchat_message.file._id(sparse/partial) so parallel confirms cannot insert 15 rows even before the JS guard.
Suggested client fix: persist at most one pending mediaConfirm per fileId; on reconnect send it once, not a queue of duplicates.
We are on 8.6.1. 8.7.0’s E2EE room-key race (#41169) is a different bug and would not make mediaConfirm idempotent. We have not reproduced on 8.7.0 yet.
Relevant logs:
Redacted nginx access (UTC, same client, same room/file):
POST /api/v1/rooms.media/{rid} 200 05:53:03
POST /api/v1/rooms.media/{rid} 200 05:53:16
POST /api/v1/rooms.mediaConfirm/{rid}/{fileId} 200 07:48:33 (x several)
POST /api/v1/rooms.mediaConfirm/{rid}/{fileId} 429 07:48:34 (x3)
POST /api/v1/rooms.mediaConfirm/{rid}/{fileId} 200 07:48:34 (continues; 15x 200 total)
No error-invalid-file-type on these requests.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the rooms.mediaConfirm entry point, Messages.getMessageByFileIdAndUsername, and sendFileMessage. Review the rocketchat_message.file._id index and the client’s pending-confirm handling described in the issue. Done means repeated or concurrent confirms produce one existing message, with tests covering the race and reconnect behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- mongodb, typescript
- Domain
- api, backend, databases, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100