RocketChat / RocketChat/Rocket.Chat

E2EE file send: rooms.mediaConfirm is not idempotent; Chrome reconnect inserts N duplicate file messages

Open
#41,886 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type: bug
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:

  1. The client successfully uploaded the encrypted blob (rooms.media HTTP 200) twice.
  2. It did not call rooms.mediaConfirm at all during that session. The sender saw “nothing happened” / send failed. The first upload stayed temporary (expiresAt +24h). The second upload sat unconfirmed.
  3. About 2 hours later, on Chrome reconnect, the client fired 18 rooms.mediaConfirm requests for the same fileId in ~300ms: 15× HTTP 200 and 3× HTTP 429.
  4. The server created 15 distinct messages, all t: "e2e", all pointing at the same application/octet-stream whose name is the SHA-256 of the original filename (by design from E2EE file encryption). msg was empty and message content was missing; encryption metadata lived only on rocketchat_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:
  1. Enable E2EE, Encrypt files, and default encryption for DMs.
  2. Open an encrypted DM in Chrome.
  3. Send a file (our case: a .doc with a non-ASCII filename, ~271 KB).
  4. 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.
  5. 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.mediaConfirm for an already confirmed fileId should return the existing message (idempotent), not insert another.
  • If rooms.media succeeded 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:03 POST /api/v1/rooms.media/{rid}200 (upload A, left with expiresAt)
  • 05:53:16 POST /api/v1/rooms.media/{rid}200 (upload B, same hash name, 277504 bytes)
  • no mediaConfirm in that session
  • 07:48:33-34 POST /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/info reports 8.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):

  1. In rooms.mediaConfirm, if Messages.getMessageByFileIdAndUsername(fileId, userId) already returns a message, return it and do not call sendFileMessage.
  2. Treat duplicate-key / already-confirmed races the same way (confirm can be concurrent).
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.