Skip to content

fix: Separate image alt text from attachment description#40839

Open
dougfabris wants to merge 3 commits into
developfrom
refactor/restore-description-render-wa17
Open

fix: Separate image alt text from attachment description#40839
dougfabris wants to merge 3 commits into
developfrom
refactor/restore-description-render-wa17

Conversation

@dougfabris
Copy link
Copy Markdown
Member

@dougfabris dougfabris commented Jun 5, 2026

Proposed changes (including videos or screenshots)

Introduces a dedicated image_alt field on image attachments to hold accessibility alternative text, separating it from description, which has been used as a visible caption and it was incorrectly pruned here

Previously the "Alternative text" input from the upload modal was stored in description and used both as the <img alt> and as a visible caption for old messages. Splitting the concerns keeps alt text accessibility-only and frees description to render old messages back again.

How to test the changes

  1. Upload an image and fill in the Alternative text field
  2. Confirm the rendered <img> uses it as alt (inspect element / screen reader)
  3. Confirm the alt text is not shown as a visible caption under the image
  4. Repeat with an E2EE room to verify the encrypted path
  5. Old messages with caption should render normally now

Issue(s)

Closes #40663

Steps to test or reproduce

Further comments

SUP-1039

Summary by CodeRabbit

  • New Features

    • Attachment descriptions are now auto-translated along with message text across supported translation providers.
    • Attachment descriptions can now be edited and are displayed in message previews, emails, PDFs, and transcripts.
    • Images now support separate accessibility alt text, distinct from descriptions.
  • Bug Fixes

    • Improved attachment description handling in message editing and display across multiple contexts.

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot Bot commented Jun 5, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 5, 2026

⚠️ No Changeset found

Latest commit: 6a164fc

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

Walkthrough

This PR extends Rocket.Chat's attachment system to treat attachment.description as a first-class field. It adds descriptionMd for parsed markdown descriptions, image_alt for image accessibility text, enables auto-translation of descriptions, refactors the file upload modal to use altText semantics, updates all attachment rendering to display descriptions with markdown support, and includes descriptions in exports and notifications.

Changes

Attachment Description System

Layer / File(s) Summary
Type contracts and attachment metadata fields
packages/core-typings/src/IMessage/MessageAttachment/*
Adds optional descriptionMd?: Root to MessageAttachmentBase for parsed markdown descriptions, and adds image_alt?: string to ImageAttachmentProps and MessageAttachmentDefault for accessibility text separate from visible descriptions.
Auto-translation for attachment descriptions
apps/meteor/app/autotranslate/client/lib/autotranslate.ts, apps/meteor/app/autotranslate/server/autotranslate.ts, apps/meteor/app/autotranslate/server/deeplTranslate.ts, apps/meteor/app/autotranslate/server/googleTranslate.ts, apps/meteor/app/autotranslate/server/msTranslate.ts
Client-side and server-side auto-translation now detects and translates attachment.description alongside attachment.text; attachment translation workflows trigger when either field is present; all translation providers (DeepL, Google, Microsoft) prioritize description over text as input.
File upload modal and alt text refactoring
apps/meteor/client/lib/chats/Upload.ts, apps/meteor/client/lib/chats/uploads.ts, apps/meteor/client/lib/chats/ChatAPI.ts, apps/meteor/client/lib/chats/flows/processMessageUploads.ts, apps/meteor/app/file-upload/server/methods/sendFileMessage.ts, apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx, apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx, apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx, apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx, apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx, apps/meteor/client/views/room/composer/messageBox/MessageComposerFileItem.tsx, apps/meteor/client/views/room/composer/messageBox/MessageComposerGenericFile.tsx
File upload refactoring separates altText (for image alt attributes) from description (user-visible captions): renames editUploadDescription to editUploadAltText throughout the uploads API and store, updates the modal form to handle altText, changes prop names in preview and composer components, and stores alt text in the image_alt attachment field.
Attachment rendering with descriptions
apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx, apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx, apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx, apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx, apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx, apps/meteor/app/file-upload/server/methods/sendFileMessage.ts
Attachment components now render descriptions with rich markdown support: uses descriptionMd via MessageContentBody when available, falls back to plain description via MarkdownText with emoji parsing; image alt attributes now use image_alt instead of description.
Message editing and display with descriptions
apps/meteor/app/ui/client/lib/ChatMessages.ts, apps/meteor/app/lib/server/methods/updateMessage.ts, apps/meteor/client/lib/normalizeThreadMessage.tsx, apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx, apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts, apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts, apps/meteor/client/components/message/toolbar/useCopyAction.ts, apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx, apps/meteor/client/hooks/useDecryptedMessage.ts, apps/meteor/app/livechat/server/lib/sendTranscript.ts
Message editing and display workflows now prefer attachment.description as primary text source: edit drafts fall back to first attachment's description, message body derivation prefers descriptions, thread/preview normalization selects descriptions when present, and decrypted message text falls back to description instead of generic placeholders.
Markdown parsing for attachment descriptions
apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts, apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts, apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts
Markdown parsing infrastructure now enriches attachment descriptions: server-side BeforeSaveMarkdownParser parses first attachment's description into descriptionMd; client-side parseMessageTextToAstMarkdown handles translated descriptions and generates markdown AST for file attachment descriptions.
Exports, notifications, and transcripts with descriptions
apps/meteor/app/lib/server/functions/notifications/email.js, apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts, apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts, apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx, apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts, apps/meteor/app/livechat/server/lib/sendTranscript.ts, ee/packages/omnichannel-services/src/OmnichannelTranscript.ts, apps/meteor/app/slackbridge/server/RocketAdapter.ts
Exports and notifications now include attachment descriptions: email notifications render descriptions below file names with emoji conversion; PDF and JSON exports preserve descriptions; transcripts and Slack adapter prioritize descriptions over original message text; EmailInbox outgoing attachment text derives from attachment descriptions.
Test updates and fixtures for description handling
apps/meteor/client/hooks/useDecryptedMessage.spec.ts, apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts, apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts, apps/meteor/tests/end-to-end/api/rooms.ts, apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts, apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts, ee/packages/federation-matrix/tests/end-to-end/messaging.spec.ts
Unit and end-to-end tests updated to verify description-based workflows: markdown parser tests assert descriptionMd is parsed only for first attachment, preview tests expect descriptions instead of titles, decryption tests mock and assert descriptions, and federation tests validate image_alt instead of description for image metadata.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • RocketChat/Rocket.Chat#40067: Parallel PR that removes description/descriptionMd rendering fallbacks—directly conflicts with this PR's expansion of description support in attachment markdown parsing and UI rendering paths.
  • RocketChat/Rocket.Chat#40075: Modifies the same image attachment rendering components (e.g., DefaultAttachment.tsx, ImageAttachment.tsx) to change how attachment text is used for the <img> alt attribute, creating dependency on this PR's separation of image_alt from description.

Suggested labels

type: bug

Suggested reviewers

  • ggazzo
  • ricardogarim
  • MartinSchoeler
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: Separate image alt text from attachment description' accurately describes the main change—introducing a dedicated image_alt field to separate accessibility alt text from visible attachment descriptions.
Linked Issues check ✅ Passed The PR successfully addresses issue #40663: restores rendering of attachment descriptions from database fields (description/descriptionMd) that became invisible in RC 8.4.x, while properly separating image_alt for accessibility purposes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue objectives: restoring attachment description rendering, introducing image_alt field, updating attachment handling across client/server, and adjusting tests accordingly. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.4.16)
apps/meteor/app/lib/server/functions/notifications/email.js

File contains syntax errors that prevent linting: Line 1: Illegal use of an import declaration outside of a module; Line 2: Illegal use of an import declaration outside of a module; Line 4: Illegal use of an import declaration outside of a module; Line 5: Illegal use of an import declaration outside of a module; Line 6: Illegal use of an import declaration outside of a module; Line 7: Illegal use of an import declaration outside of a module; Line 8: Illegal use of an import declaration outside of a module; Line 9: Illegal use of an import declaration outside of a module; Line 10: Illegal use of an import declaration outside of a module; Line 11: Illegal use of an import declaration outside of a module; Line 24: Illegal use of an export declaration outside of a module; Line 136: Illegal use of an export declaration outside of a module; Line 189: Illegal use of an export declaration outside of a module; Line 196: Illegal use of an export declaration outside of a module

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • SUP-1039: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dougfabris dougfabris added this to the 8.6.0 milestone Jun 5, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

❌ Patch coverage is 50.47619% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.03%. Comparing base (1a207e5) to head (6a164fc).
⚠️ Report is 5 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #40839      +/-   ##
===========================================
- Coverage    70.10%   70.03%   -0.08%     
===========================================
  Files         3337     3337              
  Lines       123506   123571      +65     
  Branches     22035    22023      -12     
===========================================
- Hits         86584    86540      -44     
- Misses       33585    33677      +92     
- Partials      3337     3354      +17     
Flag Coverage Δ
e2e 59.37% <22.44%> (-0.05%) ⬇️
e2e-api 46.23% <0.00%> (-0.04%) ⬇️
unit 70.81% <59.75%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dougfabris dougfabris marked this pull request as ready for review June 6, 2026 11:26
@dougfabris dougfabris requested review from a team as code owners June 6, 2026 11:26
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

7 issues found across 51 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/meteor/app/livechat/server/lib/sendTranscript.ts">

<violation number="1" location="apps/meteor/app/livechat/server/lib/sendTranscript.ts:119">
P2: `messageContent` is unconditionally replaced with `attachments[0].description || ''`, which can erase the original message text/system content for attachment messages that have no description.</violation>

<violation number="2" location="apps/meteor/app/livechat/server/lib/sendTranscript.ts:120">
P1: Attachment descriptions are inserted into transcript HTML without escaping because `escapeHtml(messageContent)` is called but its return value is ignored, enabling HTML injection in transcript output.

(Based on your team's feedback about ensuring attachment.description is actually escaped/sanitized.) [FEEDBACK_USED].</violation>
</file>

<file name="apps/meteor/client/hooks/useDecryptedMessage.ts">

<violation number="1" location="apps/meteor/client/hooks/useDecryptedMessage.ts:23">
P1: Attachment handling now overrides decrypted `msg` when both are present, causing incorrect message text to be shown. Make the attachments branch conditional on `msg` being absent (e.g., `else if`).

(Based on your team's feedback about async flow ordering and overwrite risks in this hook.) [FEEDBACK_USED].</violation>
</file>

<file name="apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts">

<violation number="1" location="apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts:145">
P2: Email body can be unintentionally blank because the new logic dropped the `message.msg` fallback when attachments have no descriptions.</violation>
</file>

<file name="apps/meteor/app/lib/server/methods/updateMessage.ts">

<violation number="1" location="apps/meteor/app/lib/server/methods/updateMessage.ts:90">
P1: The new attachment rewrite path can erase existing attachment descriptions when `updateMessage` is called without `msg` (e.g., content-only edits).</violation>
</file>

<file name="apps/meteor/app/lib/server/functions/notifications/email.js">

<violation number="1" location="apps/meteor/app/lib/server/functions/notifications/email.js:83">
P2: Index-based pairing between filtered `files` and unfiltered `attachments` can attach the wrong description to a file.</violation>
</file>

<file name="apps/meteor/client/lib/chats/uploads.ts">

<violation number="1" location="apps/meteor/client/lib/chats/uploads.ts:77">
P2: Encrypted upload metadata is being written to `altText`, which is outside the `IUpload` contract (`description` is the supported field). This can cause encrypted alt text metadata to be ignored by contract-based consumers.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment on lines +120 to +121
escapeHtml(messageContent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Attachment descriptions are inserted into transcript HTML without escaping because escapeHtml(messageContent) is called but its return value is ignored, enabling HTML injection in transcript output.

(Based on your team's feedback about ensuring attachment.description is actually escaped/sanitized.) .

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/livechat/server/lib/sendTranscript.ts, line 120:

<comment>Attachment descriptions are inserted into transcript HTML without escaping because `escapeHtml(messageContent)` is called but its return value is ignored, enabling HTML injection in transcript output.

(Based on your team's feedback about ensuring attachment.description is actually escaped/sanitized.) .</comment>

<file context>
@@ -108,14 +108,17 @@ export async function sendTranscript({
 
 		if (message.attachments && message.attachments?.length > 0) {
+			messageContent = message.attachments[0].description || '';
+			escapeHtml(messageContent);
+
 			for await (const attachment of message.attachments) {
</file context>
Suggested change
escapeHtml(messageContent);
messageContent = escapeHtml(message.attachments[0].description || '');


if (decryptedMsg.attachments && decryptedMsg.attachments.length > 0) {
setDecryptedMessage(t('Message_with_attachment'));
if (decryptedMsg.attachments && decryptedMsg.attachments?.length > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Attachment handling now overrides decrypted msg when both are present, causing incorrect message text to be shown. Make the attachments branch conditional on msg being absent (e.g., else if).

(Based on your team's feedback about async flow ordering and overwrite risks in this hook.) .

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/hooks/useDecryptedMessage.ts, line 23:

<comment>Attachment handling now overrides decrypted `msg` when both are present, causing incorrect message text to be shown. Make the attachments branch conditional on `msg` being absent (e.g., `else if`).

(Based on your team's feedback about async flow ordering and overwrite risks in this hook.) .</comment>

<file context>
@@ -18,11 +18,14 @@ export const useDecryptedMessage = (message: IMessage): string => {
 
-			if (decryptedMsg.attachments && decryptedMsg.attachments.length > 0) {
-				setDecryptedMessage(t('Message_with_attachment'));
+			if (decryptedMsg.attachments && decryptedMsg.attachments?.length > 0) {
+				if (decryptedMsg.attachments[0].description) {
+					setDecryptedMessage(decryptedMsg.attachments[0].description);
</file context>
Suggested change
if (decryptedMsg.attachments && decryptedMsg.attachments?.length > 0) {
else if (decryptedMsg.attachments?.length > 0) {

Comment on lines +90 to +94
if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
originalMessage.attachments[0].description = message.msg;
message.attachments = originalMessage.attachments;
message.msg = originalMessage.msg;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The new attachment rewrite path can erase existing attachment descriptions when updateMessage is called without msg (e.g., content-only edits).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/lib/server/methods/updateMessage.ts, line 90:

<comment>The new attachment rewrite path can erase existing attachment descriptions when `updateMessage` is called without `msg` (e.g., content-only edits).</comment>

<file context>
@@ -86,6 +86,13 @@ export async function executeUpdateMessage(
 	await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user });
 
+	// It is possible to have an empty array as the attachments property, so ensure both things exist
+	if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
+		originalMessage.attachments[0].description = message.msg;
+		message.attachments = originalMessage.attachments;
</file context>
Suggested change
if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
originalMessage.attachments[0].description = message.msg;
message.attachments = originalMessage.attachments;
message.msg = originalMessage.msg;
}
if (
typeof message.msg === 'string' &&
originalMessage.attachments &&
originalMessage.attachments.length > 0 &&
originalMessage.attachments[0].description !== undefined
) {
originalMessage.attachments[0].description = message.msg;
message.attachments = originalMessage.attachments;
message.msg = originalMessage.msg;
}

Comment on lines +119 to +120
messageContent = message.attachments[0].description || '';
escapeHtml(messageContent);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: messageContent is unconditionally replaced with attachments[0].description || '', which can erase the original message text/system content for attachment messages that have no description.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/livechat/server/lib/sendTranscript.ts, line 119:

<comment>`messageContent` is unconditionally replaced with `attachments[0].description || ''`, which can erase the original message text/system content for attachment messages that have no description.</comment>

<file context>
@@ -108,14 +108,17 @@ export async function sendTranscript({
 		let filesHTML = '';
 
 		if (message.attachments && message.attachments?.length > 0) {
+			messageContent = message.attachments[0].description || '';
+			escapeHtml(messageContent);
+
</file context>
Suggested change
messageContent = message.attachments[0].description || '';
escapeHtml(messageContent);
const attachmentDescription = message.attachments[0].description;
if (attachmentDescription) {
messageContent = escapeHtml(attachmentDescription);
}

Comment on lines +145 to +149
const emailText =
message?.attachments
?.map((a) => a.description)
.filter(Boolean)
.join('\n\n') || '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Email body can be unintentionally blank because the new logic dropped the message.msg fallback when attachments have no descriptions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts, line 145:

<comment>Email body can be unintentionally blank because the new logic dropped the `message.msg` fallback when attachments have no descriptions.</comment>

<file context>
@@ -142,7 +142,11 @@ slashCommands.add({
 		}
 
-		const emailText = message?.msg || '';
+		const emailText =
+			message?.attachments
+				?.map((a) => a.description)
</file context>
Suggested change
const emailText =
message?.attachments
?.map((a) => a.description)
.filter(Boolean)
.join('\n\n') || '';
const emailText =
message?.attachments
?.map((a) => a.description)
.filter(Boolean)
.join('\n\n') || message?.msg || '';

const attachments = message.attachments || [];
const fileParts = files.map((file, index) => {
let part = escapeHTML(file.name);
if (attachments[index]?.description) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Index-based pairing between filtered files and unfiltered attachments can attach the wrong description to a file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/lib/server/functions/notifications/email.js, line 83:

<comment>Index-based pairing between filtered `files` and unfiltered `attachments` can attach the wrong description to a file.</comment>

<file context>
@@ -77,8 +77,13 @@ export async function getEmailContent({ message, user, room }) {
+		const attachments = message.attachments || [];
+		const fileParts = files.map((file, index) => {
+			let part = escapeHTML(file.name);
+			if (attachments[index]?.description) {
+				part += `<br/><br/>${escapeHTML(attachments[index].description)}`;
+			}
</file context>

altText,
...(isEncryptedUpload(upload) && {
metadataForEncryption: { ...upload.metadataForEncryption, description },
metadataForEncryption: { ...upload.metadataForEncryption, altText },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Encrypted upload metadata is being written to altText, which is outside the IUpload contract (description is the supported field). This can cause encrypted alt text metadata to be ignored by contract-based consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/lib/chats/uploads.ts, line 77:

<comment>Encrypted upload metadata is being written to `altText`, which is outside the `IUpload` contract (`description` is the supported field). This can cause encrypted alt text metadata to be ignored by contract-based consumers.</comment>

<file context>
@@ -72,9 +72,9 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id'
+					altText,
 					...(isEncryptedUpload(upload) && {
-						metadataForEncryption: { ...upload.metadataForEncryption, description },
+						metadataForEncryption: { ...upload.metadataForEncryption, altText },
 					}),
 				};
</file context>
Suggested change
metadataForEncryption: { ...upload.metadataForEncryption, altText },
metadataForEncryption: { ...upload.metadataForEncryption, description: altText },

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts (1)

403-488: ⚡ Quick win

Add a regression test for file attachments containing both text and description.

Current additions validate translated md, but they don’t pin the descriptionMd source when both fields are present. A focused case here would prevent the file-caption markdown source from regressing again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts` around lines
403 - 488, Add a regression test that creates a file-type attachment object
containing both text and description (optionally with translation fields) and
asserts parseMessageAttachments(...) returns the description-based markdown in
the descriptionMd field (not the text). Specifically, add a test case near the
other attachment tests that calls parseMessageAttachments with an attachment
where both text and description are set and then expect the returned
[0].descriptionMd to strictly equal the parsed AST for the description value
(use same Root shape as other tests); reference parseMessageAttachments and the
descriptionMd property to locate where to assert.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/meteor/app/autotranslate/client/lib/autotranslate.ts`:
- Around line 72-80: The new description-translation branch can run after the
attachment.text branch and reuse attachment.translations[language] and
attachment.translations.original, causing both fields to be translated and the
original to be clobbered; update the logic so description and text are mutually
exclusive: if attachment.description exists prefer handling it (and skip text),
or if you choose to run text first then short-circuit and do not execute the
description branch when attachment.text was already processed; adjust checks
around attachment.text, attachment.description, attachment.translations,
language and autoTranslateShowInverse so only one of those branches mutates
attachment.translations.original or attachment.* per pass.

In `@apps/meteor/app/lib/server/methods/updateMessage.ts`:
- Around line 90-94: The update currently unconditionally sets
originalMessage.attachments[0].description = message.msg which can erase
existing captions when the update payload doesn't include text; change the logic
in updateMessage (use the originalMessage and message variables) so you only
overwrite attachments[0].description when message.msg is present (e.g., !==
undefined and not an empty string) in the incoming update; keep the assignment
of message.attachments and the swap of message.msg/message.msg restoration
scoped to that same guard so non-text edits don't clear existing descriptions.

In `@apps/meteor/app/livechat/server/lib/sendTranscript.ts`:
- Line 120: The call to escapeHtml currently discards its return value, leaving
messageContent unescaped and opening an XSS vector; update the sendTranscript
flow so the escaped string is used when building the HTML (e.g., assign the
result of escapeHtml(messageContent) back to messageContent or to a new variable
and use that variable in the template). Locate the escapeHtml invocation and the
HTML interpolation in the sendTranscript function (in sendTranscript.ts) and
replace the discarded call with an assignment so the sanitized value is what
gets inserted into the transcript HTML.
- Around line 119-120: The code currently overwrites messageContent with
message.attachments[0].description for any message; change it to only use the
attachment description when the message is not a system message so
already-formatted/sanitized system messages (the content produced earlier with
DOMPurify) are preserved. Concretely, check the message type/flag that
identifies system messages (e.g., a property like message.system or message.type
=== 'system') before assigning messageContent =
message.attachments[0].description, and keep the existing DOMPurify-sanitized
messageContent for system messages; still call escapeHtml(messageContent) after
deciding which content to use. Ensure you update the logic around
messageContent, message.attachments, and the escapeHtml call so system messages
are never overwritten by attachment descriptions.

In `@apps/meteor/app/slackbridge/server/RocketAdapter.ts`:
- Around line 206-216: The code can produce "undefined <fileName>" when
rocketMessage.msg and attachment.description are both missing; in RocketAdapter
(around getMessageAttachment and the slack.postMessage call) initialize text to
an empty string (e.g., text = rocketMessage.msg || '') and when setting from the
attachment only use attachment.description if present (e.g., if (!text &&
attachment.description) text = attachment.description), then build the msg
payload passed to slack.postMessage (slack.postMessage(..., { ...rocketMessage,
msg: `${text} ${fileName}` })) so it never interpolates undefined.

In `@apps/meteor/client/hooks/useDecryptedMessage.spec.ts`:
- Line 66: The final assertion in the useDecryptedMessage test is synchronous
and races the async decrypt flow; change the immediate
expect(result.current).toBe('Attachment description') to be wrapped in a waitFor
so the assertion waits for state updates from the hook (use the
testing-library's waitFor and assert inside it, e.g. waitFor(() =>
expect(result.current).toBe(...))). Update the test in
useDecryptedMessage.spec.ts where result.current is asserted to use waitFor to
make the check reliable.

In `@apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts`:
- Around line 75-79: The current logic sets attachment.descriptionMd by calling
textToMessageToken(text, parseOptions) which may use attachment.text instead of
the actual attachment.description; update the branch inside the isFileAttachment
block (the conditional that sets attachment.descriptionMd) to call
textToMessageToken(attachment.description, parseOptions) (while preserving the
translated || isEncryptedMessageAttachment(attachment) condition and the
existing fallback to attachment.descriptionMd) so the markdown is parsed from
attachment.description itself.

---

Nitpick comments:
In `@apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts`:
- Around line 403-488: Add a regression test that creates a file-type attachment
object containing both text and description (optionally with translation fields)
and asserts parseMessageAttachments(...) returns the description-based markdown
in the descriptionMd field (not the text). Specifically, add a test case near
the other attachment tests that calls parseMessageAttachments with an attachment
where both text and description are set and then expect the returned
[0].descriptionMd to strictly equal the parsed AST for the description value
(use same Root shape as other tests); reference parseMessageAttachments and the
descriptionMd property to locate where to assert.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4083b201-8379-47d4-9651-8af8a44e63f9

📥 Commits

Reviewing files that changed from the base of the PR and between 0533748 and 6a164fc.

📒 Files selected for processing (51)
  • apps/meteor/app/autotranslate/client/lib/autotranslate.ts
  • apps/meteor/app/autotranslate/server/autotranslate.ts
  • apps/meteor/app/autotranslate/server/deeplTranslate.ts
  • apps/meteor/app/autotranslate/server/googleTranslate.ts
  • apps/meteor/app/autotranslate/server/msTranslate.ts
  • apps/meteor/app/file-upload/server/methods/sendFileMessage.ts
  • apps/meteor/app/lib/server/functions/notifications/email.js
  • apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
  • apps/meteor/app/lib/server/methods/updateMessage.ts
  • apps/meteor/app/livechat/server/lib/sendTranscript.ts
  • apps/meteor/app/slackbridge/server/RocketAdapter.ts
  • apps/meteor/app/ui/client/lib/ChatMessages.ts
  • apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx
  • apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx
  • apps/meteor/client/components/message/toolbar/useCopyAction.ts
  • apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx
  • apps/meteor/client/hooks/useDecryptedMessage.spec.ts
  • apps/meteor/client/hooks/useDecryptedMessage.ts
  • apps/meteor/client/lib/chats/ChatAPI.ts
  • apps/meteor/client/lib/chats/Upload.ts
  • apps/meteor/client/lib/chats/flows/processMessageUploads.ts
  • apps/meteor/client/lib/chats/uploads.ts
  • apps/meteor/client/lib/normalizeThreadMessage.tsx
  • apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts
  • apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts
  • apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts
  • apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts
  • apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx
  • apps/meteor/client/views/room/composer/messageBox/MessageComposerFileItem.tsx
  • apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx
  • apps/meteor/client/views/room/composer/messageBox/MessageComposerGenericFile.tsx
  • apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts
  • apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx
  • apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts
  • apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx
  • apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx
  • apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx
  • apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx
  • apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts
  • apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts
  • apps/meteor/tests/end-to-end/api/rooms.ts
  • apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts
  • apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts
  • ee/packages/federation-matrix/tests/end-to-end/messaging.spec.ts
  • ee/packages/omnichannel-services/src/OmnichannelTranscript.ts
  • packages/core-typings/src/IMessage/MessageAttachment/Files/ImageAttachmentProps.ts
  • packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts
  • packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts

Comment on lines +72 to +80
if (attachment.description && attachment.translations && attachment.translations[language]) {
attachment.translations.original = attachment.description;

if (autoTranslateShowInverse) {
attachment.description = attachment.translations.original;
} else {
attachment.description = attachment.translations[language];
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent dual-field translation overwrite when both text and description exist.

At Line 72, this new branch can run right after the existing attachment.text branch, and both reuse attachment.translations[language] + attachment.translations.original. For attachments containing both fields, you end up translating both to the same value and overwriting the saved original, which breaks inverse/original toggling.

💡 Suggested fix (make description/text translation mutually exclusive)
-				if (attachment.text && attachment.translations && attachment.translations[language]) {
-					attachment.translations.original = attachment.text;
-
-					if (autoTranslateShowInverse) {
-						attachment.text = attachment.translations.original;
-					} else {
-						attachment.text = attachment.translations[language];
-					}
-				}
-
-				if (attachment.description && attachment.translations && attachment.translations[language]) {
+				if (attachment.description && attachment.translations && attachment.translations[language]) {
 					attachment.translations.original = attachment.description;
 
 					if (autoTranslateShowInverse) {
 						attachment.description = attachment.translations.original;
 					} else {
 						attachment.description = attachment.translations[language];
 					}
+				} else if (attachment.text && attachment.translations && attachment.translations[language]) {
+					attachment.translations.original = attachment.text;
+
+					if (autoTranslateShowInverse) {
+						attachment.text = attachment.translations.original;
+					} else {
+						attachment.text = attachment.translations[language];
+					}
 				}

Based on learnings and provided context, attachment description is now the display-oriented path and should be preferred without clobbering other fields in the same pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/autotranslate/client/lib/autotranslate.ts` around lines 72 -
80, The new description-translation branch can run after the attachment.text
branch and reuse attachment.translations[language] and
attachment.translations.original, causing both fields to be translated and the
original to be clobbered; update the logic so description and text are mutually
exclusive: if attachment.description exists prefer handling it (and skip text),
or if you choose to run text first then short-circuit and do not execute the
description branch when attachment.text was already processed; adjust checks
around attachment.text, attachment.description, attachment.translations,
language and autoTranslateShowInverse so only one of those branches mutates
attachment.translations.original or attachment.* per pass.

Comment on lines +90 to +94
if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
originalMessage.attachments[0].description = message.msg;
message.attachments = originalMessage.attachments;
message.msg = originalMessage.msg;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard attachment-description overwrite to avoid clearing captions on non-text edits.

Lines 90-94 always assign originalMessage.attachments[0].description = message.msg. If message.msg is absent in an update payload, this can wipe an existing description.

Suggested fix
-if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
+if (
+	originalMessage.attachments &&
+	originalMessage.attachments.length > 0 &&
+	originalMessage.attachments[0].description !== undefined &&
+	typeof message.msg === 'string'
+) {
 	originalMessage.attachments[0].description = message.msg;
 	message.attachments = originalMessage.attachments;
 	message.msg = originalMessage.msg;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/lib/server/methods/updateMessage.ts` around lines 90 - 94,
The update currently unconditionally sets
originalMessage.attachments[0].description = message.msg which can erase
existing captions when the update payload doesn't include text; change the logic
in updateMessage (use the originalMessage and message variables) so you only
overwrite attachments[0].description when message.msg is present (e.g., !==
undefined and not an empty string) in the incoming update; keep the assignment
of message.attachments and the swap of message.msg/message.msg restoration
scoped to that same guard so non-text edits don't clear existing descriptions.

Comment on lines +119 to +120
messageContent = message.attachments[0].description || '';
escapeHtml(messageContent);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve system message formatting instead of overwriting with attachment description.

When message.attachments is present, this code unconditionally replaces messageContent with the first attachment's description, even for system messages. System messages (line 111-113) are already formatted and sanitized via DOMPurify and should not be overwritten. This causes system messages to lose their proper formatting in transcripts.

♻️ Proposed fix to preserve system message content
-		if (message.attachments && message.attachments?.length > 0) {
+		if (message.attachments && message.attachments?.length > 0 && !messageType?.system) {
 			messageContent = message.attachments[0].description || '';
 			messageContent = escapeHtml(messageContent);
 
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/livechat/server/lib/sendTranscript.ts` around lines 119 -
120, The code currently overwrites messageContent with
message.attachments[0].description for any message; change it to only use the
attachment description when the message is not a system message so
already-formatted/sanitized system messages (the content produced earlier with
DOMPurify) are preserved. Concretely, check the message type/flag that
identifies system messages (e.g., a property like message.system or message.type
=== 'system') before assigning messageContent =
message.attachments[0].description, and keep the existing DOMPurify-sanitized
messageContent for system messages; still call escapeHtml(messageContent) after
deciding which content to use. Ensure you update the logic around
messageContent, message.attachments, and the escapeHtml call so system messages
are never overwritten by attachment descriptions.


if (message.attachments && message.attachments?.length > 0) {
messageContent = message.attachments[0].description || '';
escapeHtml(messageContent);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Assign the result of escapeHtml to prevent XSS.

The escapeHtml(messageContent) call discards its return value, leaving messageContent unescaped when interpolated into the HTML template at line 157. This creates an XSS vulnerability when attachment descriptions contain malicious HTML.

🔒 Proposed fix
 		if (message.attachments && message.attachments?.length > 0) {
 			messageContent = message.attachments[0].description || '';
-			escapeHtml(messageContent);
+			messageContent = escapeHtml(messageContent);
 
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
escapeHtml(messageContent);
if (message.attachments && message.attachments?.length > 0) {
messageContent = message.attachments[0].description || '';
messageContent = escapeHtml(messageContent);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/livechat/server/lib/sendTranscript.ts` at line 120, The call
to escapeHtml currently discards its return value, leaving messageContent
unescaped and opening an XSS vector; update the sendTranscript flow so the
escaped string is used when building the HTML (e.g., assign the result of
escapeHtml(messageContent) back to messageContent or to a new variable and use
that variable in the template). Locate the escapeHtml invocation and the HTML
interpolation in the sendTranscript function (in sendTranscript.ts) and replace
the discarded call with an assignment so the sanitized value is what gets
inserted into the transcript HTML.

Comment on lines +206 to 216
let text = rocketMessage.msg;

const attachment = this.getMessageAttachment(rocketMessage);
if (attachment) {
fileName = Meteor.absoluteUrl(attachment.title_link);
if (!text) {
text = attachment.description;
}
}

await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard text before interpolating Slack file-share message.

If rocketMessage.msg is empty and attachment.description is missing, Line 216 sends "undefined ${fileName}". Default text to '' before composing msg.

Suggested patch
-			let text = rocketMessage.msg;
+			let text = rocketMessage.msg || '';

 			const attachment = this.getMessageAttachment(rocketMessage);
 			if (attachment) {
 				fileName = Meteor.absoluteUrl(attachment.title_link);
-				if (!text) {
-					text = attachment.description;
+				if (!text) {
+					text = attachment.description || '';
 				}
 			}

-			await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` });
+			const msg = text ? `${text} ${fileName}` : fileName;
+			await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/slackbridge/server/RocketAdapter.ts` around lines 206 - 216,
The code can produce "undefined <fileName>" when rocketMessage.msg and
attachment.description are both missing; in RocketAdapter (around
getMessageAttachment and the slack.postMessage call) initialize text to an empty
string (e.g., text = rocketMessage.msg || '') and when setting from the
attachment only use attachment.description if present (e.g., if (!text &&
attachment.description) text = attachment.description), then build the msg
payload passed to slack.postMessage (slack.postMessage(..., { ...rocketMessage,
msg: `${text} ${fileName}` })) so it never interpolates undefined.

expect(result.current).toBe('E2E_message_encrypted_placeholder');
});

expect(result.current).toBe('Attachment description');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the final assertion asynchronous to avoid flaky timing.

Line 66 asserts immediately after an async decrypt flow; this can race state updates. Wrap the final expectation in waitFor.

Suggested patch
-		expect(result.current).toBe('Attachment description');
+		await waitFor(() => {
+			expect(result.current).toBe('Attachment description');
+		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result.current).toBe('Attachment description');
await waitFor(() => {
expect(result.current).toBe('Attachment description');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/hooks/useDecryptedMessage.spec.ts` at line 66, The final
assertion in the useDecryptedMessage test is synchronous and races the async
decrypt flow; change the immediate expect(result.current).toBe('Attachment
description') to be wrapped in a waitFor so the assertion waits for state
updates from the hook (use the testing-library's waitFor and assert inside it,
e.g. waitFor(() => expect(result.current).toBe(...))). Update the test in
useDecryptedMessage.spec.ts where result.current is asserted to use waitFor to
make the check reliable.

Comment on lines +75 to +79
if (isFileAttachment(attachment) && attachment.description) {
attachment.descriptionMd =
translated || isEncryptedMessageAttachment(attachment)
? textToMessageToken(text, parseOptions)
: (attachment.descriptionMd ?? textToMessageToken(text, parseOptions));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Parse descriptionMd from attachment.description, not the generalized text fallback.

On Line 78, descriptionMd is built from text, but text may resolve to attachment.text first. For file attachments that carry both fields, this can generate markdown for the wrong source and display incorrect caption content.

Suggested fix
 	if (isFileAttachment(attachment) && attachment.description) {
-		attachment.descriptionMd =
-			translated || isEncryptedMessageAttachment(attachment)
-				? textToMessageToken(text, parseOptions)
-				: (attachment.descriptionMd ?? textToMessageToken(text, parseOptions));
+		const descriptionText =
+			(isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) ||
+			attachment.description;
+		attachment.descriptionMd =
+			translated || isEncryptedMessageAttachment(attachment)
+				? textToMessageToken(descriptionText, parseOptions)
+				: (attachment.descriptionMd ?? textToMessageToken(descriptionText, parseOptions));
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts` around lines 75 -
79, The current logic sets attachment.descriptionMd by calling
textToMessageToken(text, parseOptions) which may use attachment.text instead of
the actual attachment.description; update the branch inside the isFileAttachment
block (the conditional that sets attachment.descriptionMd) to call
textToMessageToken(attachment.description, parseOptions) (while preserving the
translated || isEncryptedMessageAttachment(attachment) condition and the
existing fallback to attachment.descriptionMd) so the markdown is parsed from
attachment.description itself.

Copy link
Copy Markdown

@hacktron-app hacktron-app Bot left a comment

Choose a reason for hiding this comment

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

5 issues found across 4 files

Severity Count
HIGH 4
MEDIUM 1

View full scan results

_id: upload.id,
name: upload.file.name,
composedMessage: { tmid, msg: currentMsg, fileName: upload.file.name, description: upload.description },
composedMessage: { tmid, msg: currentMsg, fileName: upload.file.name, description: upload.altText },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH E2EE Fail-Open to Plaintext File Upload Transmission when E2E Room is Uninitialized or Upload is Unencrypted

In Rocket.Chat's client-side upload flow, when a user uploads a file in an end-to-end encrypted (E2EE) room, the system can silently fall back to sending the file in plaintext (unencrypted) if the E2E room instance is not yet initialized/ready or if the upload object is not marked as encrypted.

Specifically, in apps/meteor/client/lib/chats/flows/processMessageUploads.ts, the function continueSendingMessage retrieves the E2E room instance using await e2e.getInstanceByRoomId(rid). It then iterates over the files to be uploaded. If e2eRoom is undefined (e.g., due to initialization delays, network issues, or errors fetching keys) or if isEncryptedUpload(upload) evaluates to false, the code executes a fallback block that adds the file to confirmFilesQueue with an unencrypted composedMessage and continues the loop. Finally, the unencrypted files are confirmed and sent to the server via /v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id}.

This fail-open behavior violates the core security guarantee of End-to-End Encryption (E2EE). If E2EE is enabled for a room, any failure to encrypt must result in a hard failure (fail-closed), blocking the transmission and alerting the user, rather than silently transmitting sensitive files in plaintext to the server.

Steps to Reproduce
  1. Intercept or simulate a delay in E2EE room initialization so that e2e.getInstanceByRoomId(rid) returns undefined or resolves slowly.
  2. Attempt to upload a file in the E2EE room.
  3. Observe that the client-side code in continueSendingMessage silently bypasses encryption and posts the plaintext file details to the server via /v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id} without any warning or error displayed to the user.
Trace
graph TD
    subgraph SG0 ["apps/meteor/client/lib/chats/Upload.ts"]
        isEncryptedUpload["isEncryptedUpload"]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/client/lib/chats/flows/processMessageUploads.ts"]
        getHeightAndWidthFromDataUrl["getHeightAndWidthFromDataUrl"]
        getAttachmentForFile["Generates file attachment properties, including encryption metadata and dimensions for image files."]
        getEncryptedContent["getEncryptedContent"]
        continueSendingMessage{{"Processes queued file uploads and sends confirmation messages to the server."}}
        processMessageUploads["processMessageUploads"]
        onConfirm["onConfirm"]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/client/lib/e2ee/crypto/aes.ts"]
        encrypt["encrypt"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/client/lib/e2ee/crypto/shared.ts"]
        encryptBuffer["encryptBuffer"]
        ._Rocket.Chat_apps_meteor_client_lib_e2ee_crypto_shared.ts["./Rocket.Chat/apps/meteor/client/lib/e2ee/crypto/shared.ts"]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/client/lib/e2ee/logger.ts"]
        isDebugEnabled["isDebugEnabled"]
        Logger.constructor["Logger.constructor"]
        Logger.span["Logger.span"]
        Span.constructor["Span.constructor"]
        Span.log["Span.log"]
        Span.set["Span.set"]
        Span.info["Span.info"]
        Span.warn["Span.warn"]
        Span.error["Span.error"]
        createLogger["createLogger"]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts"]
        filterMutation["filterMutation"]
        E2ERoom.setState["E2ERoom.setState"]
        E2ERoom.isReady["Checks if the E2E room state is 'READY'."]
        E2ERoom.shouldConvertSentMessages["Determines if a message being sent should be encrypted based on room state."]
        E2ERoom.onRoomKeyReset["Handles room key reset events by clearing session keys and updating state."]
        E2ERoom.encryptText["Encrypts text content using AES-CTR and returns the encrypted data."]
        E2ERoom.encryptMessageContent["Prepares and serializes message content for E2E encryption."]
        ._Rocket.Chat_apps_meteor_client_lib_e2ee_rocketchat.e2e.room.ts["Defines constants and symbols for E2E room management including logger and internal state keys."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/client/lib/e2ee/rocketchat.e2e.ts"]
        E2E.waitForRoom["Waits for a room to be available in the global room store."]
        E2E.getInstanceByRoomId["Retrieves or creates an E2E room instance for a specific room ID."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/client/lib/toast.ts"]
        dispatchToastMessage["Dispatches a toast notification message to be displayed in the UI."]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/client/lib/utils/getConfig.ts"]
        getConfig["getConfig"]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/lib/utils/getFileExtension.ts"]
        getFileExtension["Extracts the file extension from a filename string."]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["packages/base64/src/base64.ts"]
        getChar["getChar"]
        newBinary["newBinary"]
        encode["Encodes a string or byte array into a Base64-encoded string."]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    continueSendingMessage --> dispatchToastMessage
    continueSendingMessage --> E2ERoom.shouldConvertSentMessages
    continueSendingMessage --> E2ERoom.encryptMessageContent
    continueSendingMessage --> E2E.getInstanceByRoomId
    continueSendingMessage --> isEncryptedUpload
    continueSendingMessage --> getEncryptedContent
    E2ERoom.shouldConvertSentMessages --> E2ERoom.isReady
    E2ERoom.encryptMessageContent --> encode
    E2ERoom.encryptMessageContent --> E2ERoom.encryptText
    E2E.getInstanceByRoomId --> E2ERoom.onRoomKeyReset
    E2E.getInstanceByRoomId --> ._Rocket.Chat_apps_meteor_client_lib_e2ee_rocketchat.e2e.room.ts
    E2E.getInstanceByRoomId --> E2E.waitForRoom
    getEncryptedContent --> E2ERoom.encryptMessageContent
    getEncryptedContent --> getFileExtension
    getEncryptedContent --> getAttachmentForFile
    encode --> getChar
    encode --> newBinary
    E2ERoom.encryptText --> encode
    E2ERoom.encryptText --> Logger.span
    E2ERoom.encryptText --> Span.info
    E2ERoom.encryptText --> Span.error
    E2ERoom.encryptText --> encrypt
    E2ERoom.onRoomKeyReset --> Logger.span
    E2ERoom.onRoomKeyReset --> Span.set
    E2ERoom.onRoomKeyReset --> Span.info
    E2ERoom.onRoomKeyReset --> Span.warn
    E2ERoom.onRoomKeyReset --> E2ERoom.setState
    ._Rocket.Chat_apps_meteor_client_lib_e2ee_rocketchat.e2e.room.ts --> createLogger
    getAttachmentForFile --> getFileExtension
    getAttachmentForFile --> getHeightAndWidthFromDataUrl
    Logger.span --> isDebugEnabled
    Logger.span --> Span.constructor
    Span.info --> Span.log
    Span.error --> Span.log
    Span.error --> Span.set
    encrypt --> encryptBuffer
    encrypt --> ._Rocket.Chat_apps_meteor_client_lib_e2ee_crypto_shared.ts
    Span.set --> Span.set
    Span.warn --> Span.log
    E2ERoom.setState --> Logger.span
    E2ERoom.setState --> Span.info
    E2ERoom.setState --> Span.error
    E2ERoom.setState --> filterMutation
    createLogger --> Logger.constructor
    isDebugEnabled --> getConfig
    processMessageUploads --> continueSendingMessage
    onConfirm --> continueSendingMessage
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/client/lib/chats/flows/processMessageUploads.ts
Lines: 126
Severity: high

Vulnerability: E2EE Fail-Open to Plaintext File Upload Transmission when E2E Room is Uninitialized or Upload is Unencrypted

Description:
In Rocket.Chat's client-side upload flow, when a user uploads a file in an end-to-end encrypted (E2EE) room, the system can silently fall back to sending the file in plaintext (unencrypted) if the E2E room instance is not yet initialized/ready or if the upload object is not marked as encrypted.

Specifically, in `apps/meteor/client/lib/chats/flows/processMessageUploads.ts`, the function `continueSendingMessage` retrieves the E2E room instance using `await e2e.getInstanceByRoomId(rid)`. It then iterates over the files to be uploaded. If `e2eRoom` is undefined (e.g., due to initialization delays, network issues, or errors fetching keys) or if `isEncryptedUpload(upload)` evaluates to false, the code executes a fallback block that adds the file to `confirmFilesQueue` with an unencrypted `composedMessage` and continues the loop. Finally, the unencrypted files are confirmed and sent to the server via `/v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id}`.

This fail-open behavior violates the core security guarantee of End-to-End Encryption (E2EE). If E2EE is enabled for a room, any failure to encrypt must result in a hard failure (fail-closed), blocking the transmission and alerting the user, rather than silently transmitting sensitive files in plaintext to the server.

Proof of Concept:
1. Intercept or simulate a delay in E2EE room initialization so that `e2e.getInstanceByRoomId(rid)` returns `undefined` or resolves slowly.
2. Attempt to upload a file in the E2EE room.
3. Observe that the client-side code in `continueSendingMessage` silently bypasses encryption and posts the plaintext file details to the server via `/v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id}` without any warning or error displayed to the user.

Affected Code:
- [continueSendingMessage in processMessageUploads.ts](https://github.com/RocketChat/Rocket.Chat/blob/develop/apps/meteor/client/lib/chats/flows/processMessageUploads.ts#L121-L129)
```typescript
		let content;
		if (!e2eRoom || !isEncryptedUpload(upload)) {
			confirmFilesQueue.push({
				_id: upload.id,
				name: upload.file.name,
				composedMessage: { tmid, msg: currentMsg, fileName: upload.file.name, description: upload.altText },
			});
			continue;
		}
```

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

)}
{attachment.image_url && (
<AttachmentImage {...(attachment.image_dimensions as any)} src={attachment.image_url} alt={attachment.description || ''} />
<AttachmentImage {...(attachment.image_dimensions as any)} src={attachment.image_url} alt={attachment.image_alt || ''} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Stored Cross-Site Scripting (XSS) via Unsanitized Attachment Links

The Rocket.Chat client does not validate or sanitize the protocol of URLs provided in message attachments (such as author_link and title_link) before rendering them as the href attribute of anchor (<a>) elements.

If an attacker sends a message containing an attachment with a malicious URI scheme (such as javascript:) in the author_link or title_link fields, the client will render a link pointing to that URI. When a victim clicks on the attachment's author name or title, the browser executes the malicious JavaScript payload in the context of the victim's session.

This leads to Stored Cross-Site Scripting (XSS), allowing an attacker to steal session tokens, perform unauthorized actions on behalf of the victim, or compromise the victim's account entirely.

Steps to Reproduce

An attacker can send a message payload with a custom attachment via the Rocket.Chat REST API (e.g., chat.postMessage):

{
  "roomId": "GENERAL",
  "attachments": [
    {
      "author_name": "Malicious Author",
      "author_link": "javascript:alert(document.domain)",
      "title": "Malicious Title",
      "title_link": "javascript:alert(document.cookie)"
    }
  ]
}

When any user in the channel clicks on "Malicious Author" or "Malicious Title", the JavaScript payload alert(...) will execute.

Trace
graph TD
    subgraph SG0 ["apps/meteor/app/emoji/client/emojiParser.ts"]
        emojiParser["Parses and replaces emoji shortcodes with rendered emoji elements in HTML content."]
        isElement["Type guard to check if a node is an Element."]
        isTextNode["Type guard to check if a node is a Text node."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/client/components/MarkdownText.tsx"]
        getRegexp["Generates a regular expression for validating URI schemes."]
        MarkdownText["Renders Markdown content safely by sanitizing HTML and processing tokens."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/client/components/message/content/Action.tsx"]
        Action["Action"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx"]
        applyMarkdownIfRequires["applyMarkdownIfRequires"]
        DefaultAttachment{{"Renders a default message attachment with support for Markdown and actions."}}
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/client/components/message/content/attachments/default/ActionAttachmentButton.tsx"]
        ActionAttachmentButton["Renders a button for message attachment actions."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/client/components/message/content/attachments/default/ActionAttachtment.tsx"]
        ActionAttachment["Renders a group of action buttons for an attachment."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/client/components/message/content/attachments/default/Field.tsx"]
        Field["Renders a single field in an attachment."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/client/components/message/content/attachments/default/FieldsAttachment.tsx"]
        FieldsAttachment["Renders a collection of fields for an attachment."]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/client/components/message/content/attachments/default/ShortField.tsx"]
        ShortField["A UI component that renders a field with a fixed width, used for structured attachment display."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/client/components/message/content/attachments/default/hooks/usePerformActionMutation.ts"]
        usePerformActionMutation["usePerformActionMutation"]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["apps/meteor/client/components/message/content/attachments/structure/Attachment.tsx"]
        Attachment["A container component for message attachments, applying consistent layout and styling."]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG11 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentAuthor.tsx"]
        AttachmentAuthor["A layout component for displaying the author information of an attachment."]
    end
    style SG11 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG12 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentAuthorAvatar.tsx"]
        AttachmentAuthorAvatar["Displays the avatar for an attachment's author."]
    end
    style SG12 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG13 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentAuthorName.tsx"]
        AttachmentAuthorName["Renders the author name of an attachment with truncation."]
    end
    style SG13 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG14 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentBlock.tsx"]
        AttachmentBlock["Provides a styled container block for message attachments with a colored side border."]
    end
    style SG14 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG15 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentContent.tsx"]
        AttachmentContent["Provides a styled content container for attachment details."]
    end
    style SG15 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG16 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx"]
        getDimensions["Calculates the dimensions for an image attachment based on provided limits and aspect ratio."]
        AttachmentImage["Renders an image attachment, managing loading states, error retries, and responsive sizing."]
    end
    style SG16 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG17 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentRow.tsx"]
        AttachmentRow["Provides a flex-row container for layout of attachment elements."]
    end
    style SG17 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG18 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentText.tsx"]
        AttachmentText["Renders the text content of an attachment with localization support."]
    end
    style SG18 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG19 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentThumb.tsx"]
        AttachmentThumb["Renders a thumbnail avatar for an attachment."]
    end
    style SG19 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG20 ["apps/meteor/client/components/message/content/attachments/structure/AttachmentTitle.tsx"]
        AttachmentTitle["Renders the title of an attachment with truncation."]
    end
    style SG20 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG21 ["apps/meteor/client/components/message/content/attachments/structure/image/ImageBox.tsx"]
        ImageBox["A styled wrapper component for displaying images within message attachments."]
    end
    style SG21 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG22 ["apps/meteor/client/components/message/content/attachments/structure/image/Load.tsx"]
        Load["A component that renders a placeholder for an image attachment that needs to be manually loaded by the user."]
    end
    style SG22 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG23 ["apps/meteor/client/components/message/content/attachments/structure/image/Retry.tsx"]
        Retry["A component that renders a retry button for failed image attachment loads."]
    end
    style SG23 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG24 ["apps/meteor/client/components/message/content/collapsible/CollapsibleContent.tsx"]
        CollapsibleContent["Displays a toggle button to collapse or expand message content."]
    end
    style SG24 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG25 ["apps/meteor/client/components/message/hooks/useCollapse.tsx"]
        useCollapse["Hook to manage the collapsed state of message attachments."]
    end
    style SG25 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG26 ["apps/meteor/client/hooks/useExternalLink.ts"]
        useExternalLink["Returns a callback to open a URL in a new window/tab securely."]
    end
    style SG26 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG27 ["apps/meteor/client/lib/errors/InvalidUrlError.ts"]
        InvalidUrlError.constructor["InvalidUrlError.constructor"]
    end
    style SG27 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG28 ["apps/meteor/client/lib/utils/renderMessageEmoji.ts"]
        renderMessageEmoji["Renders emoji characters in a message string using the emoji parser."]
    end
    style SG28 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG29 ["apps/meteor/client/views/room/contexts/ChatContext.ts"]
        useChat["Retrieves the current chat API context for interacting with the message composer."]
    end
    style SG29 fill:#2a2a2a,stroke:#444,color:#aaa
    DefaultAttachment --> MarkdownText
    DefaultAttachment --> useCollapse
    DefaultAttachment --> FieldsAttachment
    DefaultAttachment --> ActionAttachment
    DefaultAttachment --> AttachmentContent
    DefaultAttachment --> AttachmentAuthorAvatar
    DefaultAttachment --> AttachmentBlock
    DefaultAttachment --> AttachmentTitle
    DefaultAttachment --> AttachmentRow
    DefaultAttachment --> AttachmentText
    DefaultAttachment --> AttachmentThumb
    DefaultAttachment --> AttachmentImage
    DefaultAttachment --> AttachmentAuthorName
    DefaultAttachment --> AttachmentAuthor
    DefaultAttachment --> applyMarkdownIfRequires
    MarkdownText --> renderMessageEmoji
    MarkdownText --> getRegexp
    useCollapse --> CollapsibleContent
    FieldsAttachment --> Field
    FieldsAttachment --> ShortField
    ActionAttachment --> useExternalLink
    ActionAttachment --> ActionAttachmentButton
    AttachmentBlock --> Attachment
    AttachmentImage --> ImageBox
    AttachmentImage --> Load
    AttachmentImage --> Retry
    AttachmentImage --> getDimensions
    applyMarkdownIfRequires --> MarkdownText
    renderMessageEmoji --> emojiParser
    CollapsibleContent --> Action
    ShortField --> Field
    useExternalLink --> InvalidUrlError.constructor
    ActionAttachmentButton --> usePerformActionMutation
    Load --> ImageBox
    Retry --> ImageBox
    emojiParser --> isElement
    emojiParser --> isTextNode
    usePerformActionMutation --> useChat
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx
Lines: 100
Severity: high

Vulnerability: Stored Cross-Site Scripting (XSS) via Unsanitized Attachment Links

Description:
The Rocket.Chat client does not validate or sanitize the protocol of URLs provided in message attachments (such as `author_link` and `title_link`) before rendering them as the `href` attribute of anchor (`<a>`) elements. 

If an attacker sends a message containing an attachment with a malicious URI scheme (such as `javascript:`) in the `author_link` or `title_link` fields, the client will render a link pointing to that URI. When a victim clicks on the attachment's author name or title, the browser executes the malicious JavaScript payload in the context of the victim's session.

This leads to Stored Cross-Site Scripting (XSS), allowing an attacker to steal session tokens, perform unauthorized actions on behalf of the victim, or compromise the victim's account entirely.

Proof of Concept:
An attacker can send a message payload with a custom attachment via the Rocket.Chat REST API (e.g., `chat.postMessage`):

```json
{
  "roomId": "GENERAL",
  "attachments": [
    {
      "author_name": "Malicious Author",
      "author_link": "javascript:alert(document.domain)",
      "title": "Malicious Title",
      "title_link": "javascript:alert(document.cookie)"
    }
  ]
}
```

When any user in the channel clicks on "Malicious Author" or "Malicious Title", the JavaScript payload `alert(...)` will execute.

Affected Code:
- In [DefaultAttachment.tsx](./Rocket.Chat/apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx), the `author_link` and `title_link` properties from the attachment payload are directly passed to the `href` attribute of the `AttachmentAuthorName` and `AttachmentTitle` components (which render as `<a>` tags):
```typescript
{attachment.author_name && (
    <AttachmentAuthor>
        {attachment.author_icon && <AttachmentAuthorAvatar url={attachment.author_icon} />}
        <AttachmentAuthorName
            {...(attachment.author_link && {
                is: 'a',
                href: attachment.author_link,
                target: '_blank',
                color: undefined,
            })}
        >
            {attachment.author_name}
        </AttachmentAuthorName>
    </AttachmentAuthor>
)}
```
and
```typescript
{attachment.title && (
    <AttachmentRow>
        <AttachmentTitle
            {...(attachment.title_link && {
                is: 'a',
                href: attachment.title_link,
                target: '_blank',
                color: undefined,
            })}
        >
            {attachment.title}
        </AttachmentTitle>{' '}
        {collapse}
    </AttachmentRow>
)}
```

- In [QuoteAttachment.tsx](./Rocket.Chat/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx), the same pattern is used for `author_link`:
```typescript
<AttachmentAuthorName
    {...(attachment.author_link && { is: 'a', href: attachment.author_link, target: '_blank', color: 'default' })}
>
    {attachment.author_name}
</AttachmentAuthorName>
```

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

Comment on lines +111 to +121
let messageContent = messageType?.system
? DOMPurify.sanitize(`
<i>${messageType.text(i18n.cloneInstance({ interpolation: { escapeValue: false } }).t, message)}}</i>`)
: escapeHtml(message.msg);

let filesHTML = '';

if (message.attachments && message.attachments?.length > 0) {
messageContent = message.attachments[0].description || '';
escapeHtml(messageContent);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Stored XSS in Livechat Transcript Email

The sendTranscript function in apps/meteor/app/livechat/server/lib/sendTranscript.ts fails to properly sanitize the description field of message attachments when generating the transcript email. Specifically, the escapeHtml function is called on messageContent on line 120, but the result is not assigned back to messageContent, leaving the variable containing the raw, potentially malicious, input. This raw input is then rendered directly into the HTML of the transcript email on line 157, leading to a Stored Cross-Site Scripting (XSS) vulnerability in the email client of the recipient.

Trace
graph TD
    subgraph SG0 ["apps/meteor/app/file-upload/server/lib/FileUpload.ts"]
        FileUpload.addExtensionTo["Ensures a file has the correct extension based on its MIME type."]
        FileUpload.getStoreByName["Retrieves a file store handler by name."]
        FileUpload.getBuffer["Retrieves file content as a Buffer."]
        FileUpload.copy["Copies file content to a target stream."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/lib/server/lib/notifyListener.ts"]
        notifyOnSettingChanged["notifyOnSettingChanged"]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/livechat/server/lib/sendTranscript.ts"]
        sendTranscript{{"Generates and sends a conversation transcript via email to the requested address."}}
        escapeHtml["escapeHtml"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/mailer/server/api.ts"]
        replacekey["Replaces a specific key in a string with a given value using a regular expression."]
        translate["Translates a string by replacing language keys with their internationalized values."]
        replace["Replaces template variables in a string with values from a provided data object and global settings."]
        replaceEscaped["replaceEscaped"]
        wrap["Wraps email content with a header and footer template, applying CSS inlining."]
        checkAddressFormat["checkAddressFormat"]
        sendNoWrap["Sends an email without applying standard wrapping, triggering email-related app events."]
        send["Sends a formatted email by replacing template variables and wrapping the content."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/mailer/server/replaceVariables.ts"]
        replaceVariables["replaceVariables"]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/app/settings/server/CachedSettings.ts"]
        CachedSettings.get["Retrieves the value of a setting from the cache."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/app/utils/server/lib/getTimezone.ts"]
        padOffset["padOffset"]
        guessTimezoneFromOffset["Guesses timezone from a UTC offset."]
        getTimezone["Retrieves the timezone for a user or system default."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/lib/utils/stringUtils.ts"]
        makeString["Converts an input to a string, returning an empty string if the input is falsy."]
        strLeft["Extracts the substring to the left of the first occurrence of a separator."]
        strRightBack["Extracts the substring to the right of the last occurrence of a separator."]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["packages/logger/src/index.ts"]
        Logger.debug["Logs a debug-level message."]
        Logger.error["Logs an error-level message."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["packages/message-types/src/MessageTypes.ts"]
        MessageTypes.getType["Retrieves the message type configuration for a given message."]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    sendTranscript --> CachedSettings.get
    sendTranscript --> Logger.debug
    sendTranscript --> Logger.error
    sendTranscript --> getTimezone
    sendTranscript --> MessageTypes.getType
    sendTranscript --> FileUpload.getBuffer
    sendTranscript --> replace
    sendTranscript --> send
    sendTranscript --> escapeHtml
    CachedSettings.get --> CachedSettings.get
    Logger.debug --> Logger.debug
    Logger.error --> Logger.error
    getTimezone --> CachedSettings.get
    getTimezone --> guessTimezoneFromOffset
    FileUpload.getBuffer --> FileUpload.getStoreByName
    FileUpload.getBuffer --> FileUpload.copy
    replace --> CachedSettings.get
    replace --> strLeft
    replace --> strRightBack
    replace --> replacekey
    replace --> translate
    replace --> replace
    send --> replace
    send --> wrap
    send --> sendNoWrap
    escapeHtml --> replace
    guessTimezoneFromOffset --> padOffset
    FileUpload.copy --> FileUpload.addExtensionTo
    FileUpload.copy --> FileUpload.getStoreByName
    FileUpload.copy --> FileUpload.copy
    strLeft --> makeString
    strRightBack --> makeString
    replacekey --> replace
    translate --> replaceVariables
    wrap --> CachedSettings.get
    wrap --> replace
    wrap --> replaceEscaped
    sendNoWrap --> CachedSettings.get
    sendNoWrap --> checkAddressFormat
    sendNoWrap --> notifyOnSettingChanged
    replaceEscaped --> CachedSettings.get
    replaceEscaped --> replace
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/app/livechat/server/lib/sendTranscript.ts
Lines: 111-121
Severity: high

Vulnerability: Stored XSS in Livechat Transcript Email

Description:
The `sendTranscript` function in `apps/meteor/app/livechat/server/lib/sendTranscript.ts` fails to properly sanitize the `description` field of message attachments when generating the transcript email. Specifically, the `escapeHtml` function is called on `messageContent` on line 120, but the result is not assigned back to `messageContent`, leaving the variable containing the raw, potentially malicious, input. This raw input is then rendered directly into the HTML of the transcript email on line 157, leading to a Stored Cross-Site Scripting (XSS) vulnerability in the email client of the recipient.

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

Comment on lines +37 to 40
const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;
if (msgText === message.msg && !previewUrls && !message.customFields) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Stored XSS via Malicious Attachments in Message Update

Description

A Stored Cross-Site Scripting (XSS) vulnerability exists in the message update functionality. When sending a new message, Rocket.Chat validates message attachments using validateBodyAttachments to ensure that links (such as title_link, author_link, image_url, etc.) do not contain malicious javascript: pseudo-protocol URLs.

However, when a user updates an existing message via the updateMessage DDP method, the executeUpdateMessage function allows the attachments field to be updated without performing any URL validation or sanitization. An attacker can bypass the XSS protections by first sending a benign message and then updating it with a malicious attachment containing javascript: URLs. When other users click on these links in the updated message, arbitrary JavaScript code will execute in their browser context.

Impact

An attacker can execute arbitrary JavaScript code in the browser of any user who views the updated message and interacts with the malicious attachment links. This can lead to session hijacking, credential theft, or unauthorized actions performed on behalf of the victim.

Attacker Model

Any authenticated user who has permission to edit their own messages (which is typically enabled by default) can exploit this vulnerability. No administrative privileges are required.

Steps to Reproduce
  1. Send a benign message to a channel.
  2. Call the updateMessage DDP method with the message ID and a modified attachments payload:
Meteor.call('updateMessage', {
  _id: "<message_id>",
  rid: "<room_id>",
  attachments: [
    {
      title: "Click here to win a prize!",
      title_link: "javascript:alert(document.domain)"
    }
  ]
});
  1. When another user clicks the attachment title, the payload alert(document.domain) will execute.
Trace
graph TD
    subgraph SG0 ["apps/meteor/app/authorization/server/functions/canAccessRoom.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts["Exports server-side room access authorization functions and defines attributes for room access checks."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/authorization/server/functions/canSendMessage.ts"]
        validateRoomMessagePermissionsAsync["Validates if a user has permissions to send a message to a specific room, checking archiving, block status, read-only status, and muting."]
        canSendMessageAsync["Checks if a user can send a message to a room by first validating permissions."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/authorization/server/functions/hasPermission.ts"]
        hasPermissionAsync["Checks if a user has a specific permission within a given scope."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/lib/server/methods/updateMessage.ts"]
        executeUpdateMessage{{"Validates and processes an update to an existing message, checking permissions and time limits."}}
        updateMessage["updateMessage"]
        applyAirGappedRestrictionsValidation.arg1["applyAirGappedRestrictionsValidation.arg1"]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/settings/server/CachedSettings.ts"]
        CachedSettings.get["Retrieves the value of a setting from the cache."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Overrides Meteor's default userId to use the application's reactive Zustand store."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/client/meteor/user.ts"]
        watchUserId["watchUserId"]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/client/meteor/watch.ts"]
        watch["watch"]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/server/lib/rooms/roomCoordinator.ts"]
        RoomCoordinatorServer.allowMemberAction["RoomCoordinatorServer.allowMemberAction"]
        RoomCoordinatorServer.getRoomDirectives["Retrieves directives for a specific room type."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/server/services/authorization/service.ts"]
        Authorization.hasPermission["Checks if a user has a specific permission."]
        Authorization.all["Authorization.all"]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    executeUpdateMessage --> CachedSettings.get
    executeUpdateMessage --> hasPermissionAsync
    executeUpdateMessage --> canSendMessageAsync
    executeUpdateMessage --> updateMessage
    CachedSettings.get --> CachedSettings.get
    hasPermissionAsync --> Authorization.hasPermission
    canSendMessageAsync --> validateRoomMessagePermissionsAsync
    updateMessage --> Meteor.userId
    updateMessage --> executeUpdateMessage
    Authorization.hasPermission --> Authorization.all
    validateRoomMessagePermissionsAsync --> hasPermissionAsync
    validateRoomMessagePermissionsAsync --> ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts
    validateRoomMessagePermissionsAsync --> RoomCoordinatorServer.allowMemberAction
    validateRoomMessagePermissionsAsync --> RoomCoordinatorServer.getRoomDirectives
    Meteor.userId --> watchUserId
    watchUserId --> watch
    applyAirGappedRestrictionsValidation.arg1 --> executeUpdateMessage
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/app/lib/server/methods/updateMessage.ts
Lines: 37-95
Severity: high

Vulnerability: Stored XSS via Malicious Attachments in Message Update

Description:
### Description
A Stored Cross-Site Scripting (XSS) vulnerability exists in the message update functionality. When sending a new message, Rocket.Chat validates message attachments using `validateBodyAttachments` to ensure that links (such as `title_link`, `author_link`, `image_url`, etc.) do not contain malicious `javascript:` pseudo-protocol URLs. 

However, when a user updates an existing message via the `updateMessage` DDP method, the `executeUpdateMessage` function allows the `attachments` field to be updated without performing any URL validation or sanitization. An attacker can bypass the XSS protections by first sending a benign message and then updating it with a malicious attachment containing `javascript:` URLs. When other users click on these links in the updated message, arbitrary JavaScript code will execute in their browser context.

### Impact
An attacker can execute arbitrary JavaScript code in the browser of any user who views the updated message and interacts with the malicious attachment links. This can lead to session hijacking, credential theft, or unauthorized actions performed on behalf of the victim.

### Attacker Model
Any authenticated user who has permission to edit their own messages (which is typically enabled by default) can exploit this vulnerability. No administrative privileges are required.

Proof of Concept:
1. Send a benign message to a channel.
2. Call the `updateMessage` DDP method with the message ID and a modified `attachments` payload:
```javascript
Meteor.call('updateMessage', {
  _id: "<message_id>",
  rid: "<room_id>",
  attachments: [
    {
      title: "Click here to win a prize!",
      title_link: "javascript:alert(document.domain)"
    }
  ]
});
```
3. When another user clicks the attachment title, the payload `alert(document.domain)` will execute.

Affected Code:
- User calls the `updateMessage` DDP method: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:109)
- `executeUpdateMessage` is invoked: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:16)
- The keys of the updated message are checked against `allowedEditedFields`: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:26-32)
- Since `attachments` is in `allowedEditedFields`, the check passes: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:14)
- No validation (such as `validateBodyAttachments` or `validateMessage` from `sendMessage.ts`) is performed on the updated `attachments` array.
- The message is saved via `updateMessage`: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:98)

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

Comment on lines +37 to 40
const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;
if (msgText === message.msg && !previewUrls && !message.customFields) {
return;
}
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 Bypass of message-impersonate Permission via Message Update

Description

An authorization bypass vulnerability exists in the message update functionality, allowing users to bypass the message-impersonate permission restriction.

In Rocket.Chat, the message-impersonate permission is required to send messages with a custom alias or avatar. This is enforced during message creation in validateMessage (called by sendMessage). However, when updating an existing message via executeUpdateMessage, the fields alias and avatar are included in allowedEditedFields, but the function fails to verify whether the user has the message-impersonate permission if these fields are modified.

Consequently, any user who is allowed to edit their own messages can bypass this restriction by sending a normal message and immediately updating it to have any custom alias (e.g., "Admin" or "System Bot") and custom avatar, effectively impersonating other users or system services.

Impact

Authenticated users can impersonate administrators, moderators, or system bots within any channel they have access to, potentially leading to highly convincing social engineering attacks, phishing, or spreading misinformation.

Attacker Model

Any authenticated user who has permission to edit their own messages can exploit this vulnerability. No administrative privileges are required.

Steps to Reproduce
  1. Send a normal message to a channel.
  2. Call the updateMessage DDP method with the message ID and a modified alias and avatar:
Meteor.call('updateMessage', {
  _id: "<message_id>",
  rid: "<room_id>",
  alias: "Administrator",
  avatar: "https://example.com/admin-avatar.png",
  msg: "The system will undergo maintenance in 5 minutes. Please log out."
});
  1. The message will be updated and displayed to other users as if it was sent by "Administrator" with the specified avatar, bypassing the message-impersonate permission check.
Trace
graph TD
    subgraph SG0 ["apps/meteor/app/authorization/server/functions/canAccessRoom.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts["Exports server-side room access authorization functions and defines attributes for room access checks."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/authorization/server/functions/canSendMessage.ts"]
        validateRoomMessagePermissionsAsync["Validates if a user has permissions to send a message to a specific room, checking archiving, block status, read-only status, and muting."]
        canSendMessageAsync["Checks if a user can send a message to a room by first validating permissions."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/authorization/server/functions/hasPermission.ts"]
        hasPermissionAsync["Checks if a user has a specific permission within a given scope."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/lib/server/methods/updateMessage.ts"]
        executeUpdateMessage{{"Validates and processes an update to an existing message, checking permissions and time limits."}}
        updateMessage["updateMessage"]
        applyAirGappedRestrictionsValidation.arg1["applyAirGappedRestrictionsValidation.arg1"]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/settings/server/CachedSettings.ts"]
        CachedSettings.get["Retrieves the value of a setting from the cache."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Overrides Meteor's default userId to use the application's reactive Zustand store."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/client/meteor/user.ts"]
        watchUserId["watchUserId"]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/client/meteor/watch.ts"]
        watch["watch"]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/server/lib/rooms/roomCoordinator.ts"]
        RoomCoordinatorServer.allowMemberAction["RoomCoordinatorServer.allowMemberAction"]
        RoomCoordinatorServer.getRoomDirectives["Retrieves directives for a specific room type."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/server/services/authorization/service.ts"]
        Authorization.hasPermission["Checks if a user has a specific permission."]
        Authorization.all["Authorization.all"]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    executeUpdateMessage --> CachedSettings.get
    executeUpdateMessage --> hasPermissionAsync
    executeUpdateMessage --> canSendMessageAsync
    executeUpdateMessage --> updateMessage
    CachedSettings.get --> CachedSettings.get
    hasPermissionAsync --> Authorization.hasPermission
    canSendMessageAsync --> validateRoomMessagePermissionsAsync
    updateMessage --> Meteor.userId
    updateMessage --> executeUpdateMessage
    Authorization.hasPermission --> Authorization.all
    validateRoomMessagePermissionsAsync --> hasPermissionAsync
    validateRoomMessagePermissionsAsync --> ._Rocket.Chat_apps_meteor_app_authorization_server_functions_canAccessRoom.ts
    validateRoomMessagePermissionsAsync --> RoomCoordinatorServer.allowMemberAction
    validateRoomMessagePermissionsAsync --> RoomCoordinatorServer.getRoomDirectives
    Meteor.userId --> watchUserId
    watchUserId --> watch
    applyAirGappedRestrictionsValidation.arg1 --> executeUpdateMessage
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/app/lib/server/methods/updateMessage.ts
Lines: 37-95
Severity: medium

Vulnerability: Bypass of message-impersonate Permission via Message Update

Description:
### Description
An authorization bypass vulnerability exists in the message update functionality, allowing users to bypass the `message-impersonate` permission restriction. 

In Rocket.Chat, the `message-impersonate` permission is required to send messages with a custom `alias` or `avatar`. This is enforced during message creation in `validateMessage` (called by `sendMessage`). However, when updating an existing message via `executeUpdateMessage`, the fields `alias` and `avatar` are included in `allowedEditedFields`, but the function fails to verify whether the user has the `message-impersonate` permission if these fields are modified.

Consequently, any user who is allowed to edit their own messages can bypass this restriction by sending a normal message and immediately updating it to have any custom alias (e.g., "Admin" or "System Bot") and custom avatar, effectively impersonating other users or system services.

### Impact
Authenticated users can impersonate administrators, moderators, or system bots within any channel they have access to, potentially leading to highly convincing social engineering attacks, phishing, or spreading misinformation.

### Attacker Model
Any authenticated user who has permission to edit their own messages can exploit this vulnerability. No administrative privileges are required.

Proof of Concept:
1. Send a normal message to a channel.
2. Call the `updateMessage` DDP method with the message ID and a modified `alias` and `avatar`:
```javascript
Meteor.call('updateMessage', {
  _id: "<message_id>",
  rid: "<room_id>",
  alias: "Administrator",
  avatar: "https://example.com/admin-avatar.png",
  msg: "The system will undergo maintenance in 5 minutes. Please log out."
});
```
3. The message will be updated and displayed to other users as if it was sent by "Administrator" with the specified avatar, bypassing the `message-impersonate` permission check.

Affected Code:
- User calls the `updateMessage` DDP method: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:109)
- `executeUpdateMessage` is invoked: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:16)
- The keys of the updated message are checked against `allowedEditedFields`: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:26-32)
- Since `alias` and `avatar` are in `allowedEditedFields`, the check passes: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:14)
- The function checks if the user has `edit-message` permission or owns the message: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:52-61)
- The function does NOT check if the user has the `message-impersonate` permission when `alias` or `avatar` are modified.
- The message is saved via `updateMessage`: [updateMessage.ts](./Rocket.Chat/apps/meteor/app/lib/server/methods/updateMessage.ts:98)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text part of messages with attachments is not visible in messages predating RC 8.4.x

1 participant