-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Race Condition in Livechat Room Creation allows multiple open rooms per visitor #38148
Description
Description:
A race condition exists in the Livechat room creation logic (getRoom), allowing a single visitor to have multiple open rooms simultaneously.
When establishing a new Livechat session—either via the getRoom internal method or the GET /api/v1/livechat/room endpoint—the server checks for an existing open room and, if none is found, proceeds to create a new one. However, this "Check-Then-Act" sequence is not atomic.
If multiple requests for the same visitor arrive within the same millisecond (e.g., due to network lag, double-clicking, or a custom widget implementation), they effectively race past the "Does a room exist?" check. Both requests receive a "No" response and proceed to create separate rooms, resulting in a single visitor owning two distinct open: true rooms in the rocketchat_room collection.
This violates the expected invariant that a visitor should only have one active conversation at a time.
Steps to reproduce:
- Use the reproduction script provided below (Node.js).
- The script registers a new visitor.
- It immediately fires two concurrent
GET /api/v1/livechat/roomrequests for that visitor. - Observe the response: Both requests return success, but with different
room._idvalues. - Check the database: The
rocketchat_roomcollection will show multiple documents withv.token: <visitor_token>,t: 'l', andopen: true.
Reproduction Script (reproduce_race.js):
const crypto = require('crypto');
const BASE_URL = 'http://localhost:3000';
// Note: Node 18+ required for native fetch, or install node-fetch
if (typeof fetch !== 'function') throw new Error('Fetch not found');
const visitorToken = crypto.randomUUID();
async function runTest() {
console.log(`Testing with Visitor Token: ${visitorToken}`);
// 1. Register Visitor
await fetch(`${BASE_URL}/api/v1/livechat/visitor`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ visitor: { token: visitorToken } })
});
// 2. Fire Concurrent Room Requests
const params = new URLSearchParams({ token: visitorToken });
const url = `${BASE_URL}/api/v1/livechat/room?${params.toString()}`;
const [resA, resB] = await Promise.all([ fetch(url), fetch(url) ]);
const jsonA = await resA.json();
const jsonB = await resB.json();
const roomA = jsonA?.room?._id;
const roomB = jsonB?.room?._id;
if (roomA && roomB && roomA !== roomB) {
console.log('🚨 RACE CONDITION CONFIRMED');
console.log(`Room A: ${roomA}`);
console.log(`Room B: ${roomB}`);
} else {
console.log('✅ Safe (No duplicates created)');
}
}
runTest();Expected behavior:
The server should serialize room creation or fail one of the requests.
- Request A: Creates Room X.
- Request B: Waits/Checks, sees Room X exists, and returns Room X.
- Result: The visitor has exactly one open room.
Actual behavior:
The server processes both requests in parallel.
- Request A: Sees no room -> Creates Room X.
- Request B: Sees no room -> Creates Room Y.
- Result: The visitor has two open rooms (Room X and Room Y).
Server Setup Information:
- Version of Rocket.Chat Server: Develop (Latest)
- Deployment Method: Source / Local
- NodeJS Version: v22.16.0
- MongoDB Version: N/A (Standard ReplicaSet)
Additional context
Root Cause Analysis:
The vulnerability lies in apps/meteor/app/livechat/server/lib/rooms.ts:
getRoomcallsLivechatRooms.findOneById(...)(or strict RID check).- It fails to check
findOneOpenByVisitorTokenif the specific RID isn't found. - Even if it did, the gap between "Check" and "Create" (
QueueManager.requestRoom) is unprotected by any lock or unique constraint.
Suggested Fixes for Discussion:
- Database Constraint (Strongest): Add a partial unique index on
{ "v.token": 1 }where{ open: true }. This prevents duplicates at the engine level but may require migration for legacy data. - Application Lock: Wrap the creation logic in a
Lockeror Mutex keyed byvisitorToken. - Optimistic Upsert: Refactor creation to use
findOneAndUpdatewith$setOnInsert, ensuring atomicity.
Relevant logs:
Output from the reproduction script:
Testing with Visitor Token: 6e3a7317-0760-49fe-a209-2d1cfa8c06ec
...
Response A room: 2EYJDB2cK94t4wyCL
Response B room: Bq54Dj8QGkZHAZSkE
🚨 RACE CONDITION CONFIRMED
The server created TWO different rooms for the same visitor!