Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 8 additions & 27 deletions apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { useConnectionStatus } from '@rocket.chat/ui-contexts';
import { useEffect, useRef } from 'react';

import { LegacyRoomManager, upsertMessage } from '../../../../app/ui-utils/client';
import { sdk } from '../../../../app/utils/client/lib/SDKClient';
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
import { callWithErrorHandling } from '../../../lib/utils/callWithErrorHandling';
import { Messages, Subscriptions } from '../../../stores';

/**
Expand All @@ -22,33 +21,15 @@ const loadMissedMessages = async (rid: IRoom['_id']): Promise<void> => {
}

try {
const { result } = await sdk.rest.get('/v1/chat.syncMessages', {
roomId: rid,
lastUpdate: lastMessage.ts.toISOString(),
});

if (result?.updated?.length) {
// TODO(ddp-removal): this should move to `/v1/chat.syncMessages` so reconnect
// also reconciles edits/deletions made while offline, but that endpoint queries
// by `_updatedAt` (+ trash collection) and is currently too slow for this path.
// Evaluate/optimize the query before migrating; for now keep the DDP method.
const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts);
if (result) {
const subscription = Subscriptions.state.find((record) => record.rid === rid);
// `/v1/chat.syncMessages` returns everything changed since `lastUpdate` by
// `_updatedAt`, which includes edits to older messages. We only want to
// upsert messages that are genuinely new (created after our newest loaded
// message) or that are already loaded (so edits stay in sync), otherwise we
// would inject stale messages into the room history.
await Promise.all(
result.updated
.map(mapMessageFromApi)
.filter((msg) => msg.ts.getTime() > lastMessage.ts.getTime() || Messages.state.has(msg._id))
.map((msg) => upsertMessage({ msg, subscription })),
);
await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription })));
}

// Drop messages that were deleted while the connection was down, but only if
// they are currently loaded.
result?.deleted?.forEach(({ _id }) => {
if (Messages.state.has(_id)) {
Messages.state.delete(_id);
}
});
} catch (error) {
console.error('Error loading missed messages:', error);
}
Expand Down
2 changes: 0 additions & 2 deletions apps/meteor/server/methods/loadMissedMessages.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM Unbounded Database Query and Resource Exhaustion in loadMissedMessages Method

The server-side Meteor method loadMissedMessages fetches missed messages in a room after a given timestamp start without enforcing any limit or pagination. It calls Messages.findVisibleByRoomIdAfterTimestamp and converts the resulting cursor directly to an array via .toArray().

If an attacker (or any authorized user of a room) calls this method with a very old timestamp (e.g., new Date(0)) in a room/channel containing a large history of messages, the server will query and load all of those messages into memory at once before serializing and sending them to the client. This can lead to severe resource exhaustion, high CPU usage, and potentially an Out Of Memory (OOM) crash of the Node.js server process, resulting in a Denial of Service (DoS) for all users of the platform.

Steps to Reproduce

An attacker can execute the following DDP method call over WebSockets for any room they have access to (e.g., a large public channel like #general):

Meteor.call('loadMissedMessages', 'GENERAL_ROOM_ID', new Date(0), (err, res) => {
    if (err) console.error(err);
    else console.log(`Fetched ${res.length} messages`);
});

If the channel has hundreds of thousands of messages, this triggers a massive database fetch and memory allocation, leading to server instability or crash.

Trace
graph TD
    subgraph SG0 ["apps/meteor/app/authorization/server/functions/canAccessRoom.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts["Exports authorization functions and room access attributes for room access control."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Overrides Meteor.userId to return the reactive userId from the local store."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/client/meteor/user.ts"]
        watchUserId["Watches the userId store and returns the reactive current user ID."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/client/meteor/watch.ts"]
        watch["watch"]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/server/methods/loadMissedMessages.ts"]
        loadMissedMessages{{"Server-side Meteor method to fetch missed messages for a room after a timestamp."}}
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    loadMissedMessages --> Meteor.userId
    loadMissedMessages --> ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts
    Meteor.userId --> watchUserId
    watchUserId --> watch
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/server/methods/loadMissedMessages.ts
Severity: medium

Vulnerability: Unbounded Database Query and Resource Exhaustion in loadMissedMessages Method

Description:
The server-side Meteor method `loadMissedMessages` fetches missed messages in a room after a given timestamp `start` without enforcing any limit or pagination. It calls `Messages.findVisibleByRoomIdAfterTimestamp` and converts the resulting cursor directly to an array via `.toArray()`. 

If an attacker (or any authorized user of a room) calls this method with a very old timestamp (e.g., `new Date(0)`) in a room/channel containing a large history of messages, the server will query and load all of those messages into memory at once before serializing and sending them to the client. This can lead to severe resource exhaustion, high CPU usage, and potentially an Out Of Memory (OOM) crash of the Node.js server process, resulting in a Denial of Service (DoS) for all users of the platform.

Proof of Concept:
An attacker can execute the following DDP method call over WebSockets for any room they have access to (e.g., a large public channel like `#general`):

```javascript
Meteor.call('loadMissedMessages', 'GENERAL_ROOM_ID', new Date(0), (err, res) => {
    if (err) console.error(err);
    else console.log(`Fetched ${res.length} messages`);
});
```

If the channel has hundreds of thousands of messages, this triggers a massive database fetch and memory allocation, leading to server instability or crash.

Affected Code:
- [loadMissedMessages.ts L17-L36](https://github.com/RocketChat/Rocket.Chat/blob/develop/apps/meteor/server/methods/loadMissedMessages.ts#L17-L36)
```typescript
		return Messages.findVisibleByRoomIdAfterTimestamp(rid, start, true, {
			sort: {
				ts: -1,
			},
		}).toArray();
```

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { canAccessRoomIdAsync } from '../../app/authorization/server/functions/canAccessRoom';
import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -16,7 +15,6 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async loadMissedMessages(rid, start) {
methodDeprecationLogger.method('loadMissedMessages', '9.0.0', '/v1/chat.syncMessages');
check(rid, String);
check(start, Date);

Expand Down
Loading