msgbyte / msgbyte/tailchat

IDOR: Reaction endpoints bypass conversation permission checks

Open
#301 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.6k
Forks
398
Avg merge
1h 14m
Merged PRs (30d)
2

Description

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.

Contributor guide

No contributing guide indexed for this repository

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 in server/services/core/chat/message.service.ts at addReaction and removeReaction, then compare their message lookup with checkConversePermission() and the other message operations listed in the issue. Ensure both reaction paths verify the message conversation and group before changing reactions; done means unauthorized users cannot affect or infer messages while permitted reaction behavior remains intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
mongodb, typescript
Domain
authorization, security
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.