Skip to content

IDOR: Reaction endpoints bypass conversation permission checks #301

@lighthousekeeper1212

Description

@lighthousekeeper1212

Summary

The addReaction and removeReaction methods in server/services/core/chat/message.service.ts bypass the checkConversePermission() check that all other message operations use. This allows any authenticated user to add/remove reactions on messages in conversations they are not a member of, by specifying the target message's MongoDB ObjectID.

Vulnerable Code

In message.service.ts, the addReaction method (around line 492) and removeReaction method (around line 533) perform a direct findById(messageId) without any permission verification:

// addReaction - NO permission check
const message = await this.adapter.model.findById(messageId);

// removeReaction - NO permission check  
const message = await this.adapter.model.findById(messageId);

Secure Comparison

All other message operations correctly call checkConversePermission() before accessing messages:

  • sendMessage (line ~201): await this.checkConversePermission(ctx, converseId, groupId)
  • getMessage / deleteMessage / recallMessage / editMessage / fetchConverseLastMessages / fetchNearbyMessage: All call checkConversePermission()

The checkConversePermission() method (line ~577) validates that the user is either a member of the group's panel, or a participant in the DM conversation.

Impact

  • Severity: Medium - Requires valid MongoDB ObjectID (not easily guessable), but allows cross-conversation reaction manipulation
  • Any authenticated user can add emoji reactions to messages in private groups/DMs they don't belong to
  • Any authenticated user can remove other users' reactions from messages they can't access
  • Reveals message existence (side-channel information disclosure)

Suggested Fix

Add checkConversePermission() call at the start of both addReaction and removeReaction:

async addReaction(ctx, messageId, emoji) {
  const message = await this.adapter.model.findById(messageId);
  if (!message) throw new Error('Message not found');
  
  // Add this permission check:
  await this.checkConversePermission(ctx, String(message.converseId), message.groupId ? String(message.groupId) : undefined);
  
  // ... rest of the method
}

Apply the same pattern to removeReaction.

Discovery

Found through automated security research comparing permission patterns across message operation endpoints.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions