Skip to content

fix: avoid to get files with other extensions than the proper ones for custom-sounds and emoji-custom endpoints#38531

Merged
ggazzo merged 22 commits intodevelopfrom
fix/sounds-emojis-get-files
Feb 24, 2026
Merged

fix: avoid to get files with other extensions than the proper ones for custom-sounds and emoji-custom endpoints#38531
ggazzo merged 22 commits intodevelopfrom
fix/sounds-emojis-get-files

Conversation

@nazabucciarelli
Copy link
Copy Markdown
Contributor

@nazabucciarelli nazabucciarelli commented Feb 6, 2026

Proposed changes (including videos or screenshots)

I propose fetching the file first and validating the contentType attribute matches the kind of the requested resource (e.g image, audio). I thought about validating by file extensions (mp3 for audio, png for emojis), but that might lead to scenarios where previous files with different extensions where uploaded because at a certain point in Rocket.Chat they were allowed, and if we don't include them then when the user fetches them, they'll get an error. So I determined this is the most backwards-compatible approach.

Edit: The approach was changed to checking whether the file exists in the collection, and if it does, then it's tried to read it and if one of these two operations fail, then Not found is returned.

Why weren't tests added?

Test were not added since they are blocked because of the impossibility of changing the storage type for sounds/emojis at runtime. A task was created for this: CORE-1843. You will notice this task is blocked by another one that consists of making the storage type reactive to changes (currently, we can't change the storage type setting in tests because to make it take effect we must restart the server).

Issue(s)

CORE-1805 Improve how custom-sounds and emoji-custom endpoints get files

Steps to test or reproduce

Further comments

Summary by CodeRabbit

  • Bug Fixes

    • Prevented cross-resource access between custom emojis and custom sounds; missing assets now return 404 or an SVG placeholder.
    • Validate asset existence before serving; assets are delivered with correct media types and proper caching/304 behavior.
  • Chores

    • Added a patch release changeset for the package.

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot bot commented Feb 6, 2026

Looks like this PR is ready to merge! 🎉
If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 6, 2026

🦋 Changeset detected

Latest commit: 6269156

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 41 packages
Name Type
@rocket.chat/meteor Patch
@rocket.chat/core-typings Patch
@rocket.chat/rest-typings Patch
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/fuselage-ui-kit Patch
@rocket.chat/gazzodown Patch
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Patch
@rocket.chat/ui-client Patch
@rocket.chat/ui-contexts Patch
@rocket.chat/ui-voip Patch
@rocket.chat/web-ui-registration Patch
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/models Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Patch
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch
@rocket.chat/server-fetch Patch

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 029d00d and 6269156.

📒 Files selected for processing (1)
  • packages/models/src/models/EmojiCustom.ts

Walkthrough

Adds model-based existence checks to custom-sounds and emoji-custom endpoints before streaming files, centralizes an SVG fallback for missing custom emojis with 304 handling, and adds a changeset declaring a patch fixing cross-resource access when using FileSystem storage mode. (34 words)

Changes

Cohort / File(s) Summary
Changeset
\.changeset/clean-ears-fly.md
New changeset declaring a patch release and noting a fix for cross-resource access between custom-sounds and emoji-custom when using FileSystem storage mode.
Custom Sounds Endpoint
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js
Import CustomSounds; perform preliminary lookup of the sound record (by derived id) before reading the file stream; return 404 if record not found; preserve existing 403 and file-missing handling.
Custom Emoji Endpoint
apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
Import EmojiCustom; add centralized writeSvgFallback(res, req) with If-Modified-Since/304 handling; lookup emoji via EmojiCustom.findOneByName before file access and use SVG fallback when missing; retain streaming, content-type and caching headers when file present.
Model typings
packages/model-typings/src/models/IEmojiCustomModel.ts
Add findOneByName(name: string, options?) method signature to the IEmojiCustomModel interface.
Model implementation
packages/models/src/models/EmojiCustom.ts
Add findOneByName(name: string, options?) method delegating to findOne({ name }, options) on the EmojiCustomRaw model.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Server as Server (custom-sounds / emoji-custom)
  participant DB as DB (CustomSounds / EmojiCustom)
  participant Store as FileStore (FileSystem)

  Client->>Server: GET /custom-sounds/:name or /emoji-custom/:name
  Server->>DB: lookup metadata (findOneById / findOneByName)
  DB-->>Server: metadata (found / not found)
  alt metadata found
    Server->>Store: attempt to read file stream
    Store-->>Server: file stream (found / not found)
    alt file found
      Server-->>Client: 200 + file stream + content-type/caching headers
    else file missing
      Server-->>Client: 404 Not Found
    end
  else metadata missing (emoji)
    Server-->>Client: 200 + fallback SVG or 304
  else metadata missing (sound)
    Server-->>Client: 404 Not Found
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
I hopped through uploads, counted every name,
Blocked the stray peek and closed the lane.
Missing emoji? I draw a tiny view,
Missing sound? a 404 rings true.
Safe files now — a carrot for the game 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main security fix: preventing cross-resource file access between custom-sounds and emoji-custom endpoints by validating resource type.
Linked Issues check ✅ Passed The PR successfully addresses CORE-1805 by implementing pre-lookup validation to prevent cross-access between custom-sounds and emoji-custom endpoints. CORE-1843 requests tests, which are deferred pending reactive storage-mode settings.
Out of Scope Changes check ✅ Passed All changes are directly related to CORE-1805: imports are added to support resource validation, lookup methods are implemented to check resource existence, and fallback mechanisms prevent accessing unrelated files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


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.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.63%. Comparing base (3048477) to head (335b64d).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #38531      +/-   ##
===========================================
- Coverage    70.64%   70.63%   -0.02%     
===========================================
  Files         3189     3189              
  Lines       112715   112712       -3     
  Branches     20363    20438      +75     
===========================================
- Hits         79627    79611      -16     
- Misses       31044    31052       +8     
- Partials      2044     2049       +5     
Flag Coverage Δ
unit 71.19% <ø> (+0.01%) ⬆️

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 6, 2026

📦 Docker Image Size Report

📈 Changes

Service Current Baseline Change Percent
sum of all images 1.1GiB 1.1GiB +11MiB
rocketchat 360MiB 349MiB +11MiB
omnichannel-transcript-service 134MiB 134MiB +2.6KiB
queue-worker-service 134MiB 134MiB -272B
ddp-streamer-service 128MiB 128MiB +2.3KiB
account-service 115MiB 115MiB -3.8KiB
authorization-service 112MiB 112MiB +1.2KiB
presence-service 112MiB 112MiB -1.5KiB

📊 Historical Trend

---
config:
  theme: "dark"
  xyChart:
    width: 900
    height: 400
---
xychart
  title "Image Size Evolution by Service (Last 30 Days + This PR)"
  x-axis ["11/28 19:05", "12/01 23:01", "12/02 21:57", "12/03 21:00", "12/04 18:17", "12/05 21:56", "12/08 20:15", "12/09 22:17", "12/10 23:26", "12/11 21:56", "12/12 22:45", "12/13 01:34", "12/15 22:31", "12/16 22:18", "12/17 21:04", "12/18 23:12", "12/19 23:27", "12/20 21:03", "12/22 18:54", "12/23 16:16", "12/24 19:38", "12/25 17:51", "12/26 13:18", "12/29 19:01", "12/30 20:52", "02/12 22:57", "02/13 22:38", "02/16 14:04", "02/18 23:15", "02/19 20:19", "02/19 20:51 (PR)"]
  y-axis "Size (GB)" 0 --> 0.5
  line "account-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11]
  line "authorization-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11]
  line "ddp-streamer-service" [0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]
  line "omnichannel-transcript-service" [0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13]
  line "presence-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11]
  line "queue-worker-service" [0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13]
  line "rocketchat" [0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.35]
Loading

Statistics (last 30 days):

  • 📊 Average: 1.5GiB
  • ⬇️ Minimum: 1.4GiB
  • ⬆️ Maximum: 1.6GiB
  • 🎯 Current PR: 1.1GiB
ℹ️ About this report

This report compares Docker image sizes from this build against the develop baseline.

  • Tag: pr-38531
  • Baseline: develop
  • Timestamp: 2026-02-19 20:51:02 UTC
  • Historical data points: 30

Updated: Thu, 19 Feb 2026 20:51:02 GMT

@nazabucciarelli
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nazabucciarelli nazabucciarelli added this to the 8.2.0 milestone Feb 9, 2026
@nazabucciarelli nazabucciarelli marked this pull request as ready for review February 19, 2026 01:10
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.

No issues found across 3 files

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/meteor/app/emoji-custom/server/startup/emoji-custom.js (1)

43-50: ⚠️ Potential issue | 🟡 Minor

split('.').pop() returns the full filename—not an empty string—for extension-less emoji names.

For params.emoji = "emojiname" (no dot), "emojiname".split('.').pop() is "emojiname", which is truthy, so the !extension guard passes. The file's contentType check at line 56 prevents serving non-image bytes, so there's no security gap, but such filenames bypass the intent of the guard and ultimately fall through to a hardcoded image/jpeg Content-Type.

Consider a stricter guard that also rejects filenames that don't contain a dot (i.e., have no recognizable extension):

🛡️ Proposed tightened guard
-		const extension = params.emoji?.split('.').pop()?.toLowerCase() ?? '';
-
-		if (!extension) {
+		const lastDot = params.emoji?.lastIndexOf('.');
+		const extension = lastDot != null && lastDot >= 0 ? params.emoji.slice(lastDot + 1).toLowerCase() : '';
+
+		if (!extension || lastDot === 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` around lines 43
- 50, The guard that derives extension using params.emoji?.split('.').pop()
incorrectly treats extension-less filenames like "emojiname" as valid; change
the check that sets extension so it only accepts names containing a dot and a
non-empty suffix (e.g., ensure params.emoji includes('.') before splitting or
use a regex to extract the part after the last dot and verify it's different
from the full name), and if no valid dot-extension is found, respond with 403 as
the current branch intends; update the logic around the extension variable (the
code that currently sets extension) and the subsequent 403 branch to enforce
this stricter validation.
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js (1)

50-57: ⚠️ Potential issue | 🟠 Major

Backward-compatibility risk: audio files without contentType metadata will silently 404.

When file.contentType is undefined, the expression file.contentType?.startsWith('audio/') evaluates to undefined (falsy), causing the guard !file || !file.contentType?.startsWith('audio/') to return true and serve a 404 for valid audio files that lack stored MIME type metadata.

This is a real issue for GridFS-backed files uploaded without explicit contentType metadata—the getFileWithReadStream implementation extracts contentType: file.contentType from database records, which can be undefined. While the FileSystem adapter mitigates this via mime.lookup() with an 'application/octet-stream' fallback, GridFS has no such guarantee. Files from pre-fix uploads may lack contentType entirely, breaking backward compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js` around lines
50 - 57, The current guard in the custom sounds route rejects files where
file.contentType is undefined; update the check in the handler that calls
RocketChatFileCustomSoundsInstance.getFileWithReadStream(fileId) to treat
undefined contentType as unknown rather than non-audio: if contentType is
missing, infer the MIME type from file.name (use the existing mime.lookup
utility or equivalent) and only 404 when the resolved mime type does not
startWith('audio/'); when streaming, set the response Content-Type to the
inferred value or fallback to 'application/octet-stream' to preserve
GridFS/backward compatibility.
🧹 Nitpick comments (1)
apps/meteor/app/emoji-custom/server/startup/emoji-custom.js (1)

101-107: Use file.contentType for the response Content-Type header instead of deriving it from the URL extension.

After the check at line 56 already validates that file.contentType.startsWith('image/'), the actual Content-Type header is set by re-deriving the type from the URL extension. If the stored contentType and the URL extension diverge (e.g., a file stored as emoji.svg with contentType: 'image/png'), the browser receives a Content-Type: image/svg+xml header while streaming PNG bytes, causing a render failure. custom-sounds.js correctly uses file.contentType directly at line 84—applying the same pattern here is simpler and eliminates the mismatch:

♻️ Proposed fix
-		if (/^svg$/i.test(extension)) {
-			res.setHeader('Content-Type', 'image/svg+xml');
-		} else if (/^png$/i.test(extension)) {
-			res.setHeader('Content-Type', 'image/png');
-		} else {
-			res.setHeader('Content-Type', 'image/jpeg');
-		}
+		res.setHeader('Content-Type', file.contentType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` around lines 101
- 107, The response Content-Type is being derived from the URL extension; change
it to use the stored MIME type instead by replacing the extension-based branch
with a single call that sets res.setHeader('Content-Type', file.contentType)
(the variable file and res are available in this scope and file.contentType was
already validated to start with "image/"); update the header-setting code in
emoji-custom.js where the extension checks occur to use file.contentType so the
streamed bytes match the declared MIME type.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13433b1 and 3a6ac61.

📒 Files selected for processing (3)
  • .changeset/clean-ears-fly.md
  • apps/meteor/app/custom-sounds/server/startup/custom-sounds.js
  • apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/app/custom-sounds/server/startup/custom-sounds.js
  • apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
🔇 Additional comments (1)
.changeset/clean-ears-fly.md (1)

1-5: LGTM — changeset description is accurate and clearly scoped.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js`:
- Around line 50-57: The current guard in the custom sounds route rejects files
where file.contentType is undefined; update the check in the handler that calls
RocketChatFileCustomSoundsInstance.getFileWithReadStream(fileId) to treat
undefined contentType as unknown rather than non-audio: if contentType is
missing, infer the MIME type from file.name (use the existing mime.lookup
utility or equivalent) and only 404 when the resolved mime type does not
startWith('audio/'); when streaming, set the response Content-Type to the
inferred value or fallback to 'application/octet-stream' to preserve
GridFS/backward compatibility.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js`:
- Around line 43-50: The guard that derives extension using
params.emoji?.split('.').pop() incorrectly treats extension-less filenames like
"emojiname" as valid; change the check that sets extension so it only accepts
names containing a dot and a non-empty suffix (e.g., ensure params.emoji
includes('.') before splitting or use a regex to extract the part after the last
dot and verify it's different from the full name), and if no valid dot-extension
is found, respond with 403 as the current branch intends; update the logic
around the extension variable (the code that currently sets extension) and the
subsequent 403 branch to enforce this stricter validation.

---

Nitpick comments:
In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js`:
- Around line 101-107: The response Content-Type is being derived from the URL
extension; change it to use the stored MIME type instead by replacing the
extension-based branch with a single call that sets
res.setHeader('Content-Type', file.contentType) (the variable file and res are
available in this scope and file.contentType was already validated to start with
"image/"); update the header-setting code in emoji-custom.js where the extension
checks occur to use file.contentType so the streamed bytes match the declared
MIME type.

@nazabucciarelli
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all 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/emoji-custom/server/startup/emoji-custom.js">

<violation number="1" location="apps/meteor/app/emoji-custom/server/startup/emoji-custom.js:59">
P2: The content-type validation was removed here, so non-image files can now be served through /emoji-custom when an emoji name exists. Since uploads accept arbitrary content types, this can return videos/other binaries as emoji responses and sets an incorrect image Content-Type. Reintroduce the contentType guard to keep non-image uploads from being served.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js (1)

51-61: ⚠️ Potential issue | 🟠 Major

file.readStream is leaked when sound is missing but file is non-null.

Same pattern as the emoji endpoint: Promise.all opens the file stream before the DB check completes. In the cross-resource scenario the getFileWithReadStream call can successfully open a stream for a file that exists on the shared filesystem while CustomSounds.findOneById returns null—resulting in a 404 early return with a live, undrained readStream.

🐛 Proposed fix: destroy the stream on early return
-		if (!file || !sound) {
+		if (!file || !sound) {
+			file?.readStream?.destroy();
 			res.writeHead(404);
 			res.write('Not found');
 			res.end();
 			return;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js` around lines
51 - 61, The current Promise.all opens the file stream via
RocketChatFileCustomSoundsInstance.getFileWithReadStream before verifying DB
absence from CustomSounds.findOneById, which can leak file.readStream when sound
is null; update the handler so that if sound is missing but file is non-null you
call file.readStream.destroy() (or file.readStream.close() if appropriate)
before writing the 404 response and returning, ensuring you reference the same
RocketChatFileCustomSoundsInstance.getFileWithReadStream result and
CustomSounds.findOneById check to locate where to insert the stream cleanup.
🧹 Nitpick comments (4)
apps/meteor/app/emoji-custom/server/startup/emoji-custom.js (3)

104-110: Content-Type is still derived from the URL extension rather than file.contentType.

custom-sounds.js line 88 uses file.contentType directly from the stored record, which reflects the actual MIME type of the uploaded file. The emoji endpoint hardcodes a mapping (svg → image/svg+xml, png → image/png, otherwise image/jpeg) based purely on the URL extension. Given the PR's goal of backward-compatible content validation, using file.contentType here would also correctly handle emojis uploaded with non-standard extensions or no extension.

♻️ Proposed refactor
-		if (/^svg$/i.test(params.emoji.split('.').pop())) {
-			res.setHeader('Content-Type', 'image/svg+xml');
-		} else if (/^png$/i.test(params.emoji.split('.').pop())) {
-			res.setHeader('Content-Type', 'image/png');
-		} else {
-			res.setHeader('Content-Type', 'image/jpeg');
-		}
+		res.setHeader('Content-Type', file.contentType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` around lines 104
- 110, Replace the URL-extension based Content-Type logic in the emoji endpoint
with the stored file's MIME type: read and use the uploaded record's
file.contentType (the same property used in custom-sounds.js) to set
res.setHeader('Content-Type', file.contentType); if file.contentType is missing
or empty, fall back to a sensible default (e.g., 'image/jpeg') to preserve
compatibility; keep references to params.emoji only for lookup, not for deriving
the MIME type.

52-53: Consider replacing countByNameOrAlias with a lighter existence check for consistency.

countByNameOrAlias executes a count aggregation and returns a number, but the result is only used as a boolean (!emojiCount). The custom-sounds endpoint uses findOneById with { projection: { _id: 1 } }, which fetches a single minimal document and is a more idiomatic existence check. A parallel findOneByNameOrAlias (if available on the model) with the same minimal projection would be slightly more efficient and symmetric.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` around lines 52
- 53, Replace the count-based existence check by using a minimal-document find:
replace the call to EmojiCustom.countByNameOrAlias(...) with
EmojiCustom.findOneByNameOrAlias(params.emoji.split('.')[0], { projection: {
_id: 1 } }) (or implement/find an equivalent finder), update the destructured
variable name from emojiCount to something like emojiDoc (or emojiExistsDoc),
and change the boolean check from !emojiCount to !emojiDoc to preserve behavior;
this mirrors the custom-sounds pattern and avoids a full count aggregation.

52-55: Wrap the Promise.all body in a try/catch to prevent response hangs on DB/IO errors.

EmojiCustom.countByNameOrAlias is a new DB operation. If it (or getFileWithReadStream) throws—e.g., on a MongoDB connection error—the rejected promise from the async handler is unhandled by WebApp.connectHandlers. Errors thrown inside async functions without a catch block result in unhandled promise rejections, which in Node.js 3.x+ can terminate the process, and in all versions leave the response socket open and the client hanging.

♻️ Proposed fix
 	return WebApp.connectHandlers.use('/emoji-custom/', async (req, res /* , next*/) => {
+		try {
 		const params = { emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) };
 		// ... rest of handler ...
 		file.readStream.pipe(res);
+		} catch (err) {
+			SystemLogger.error({ msg: 'Error serving emoji-custom', err });
+			if (!res.headersSent) {
+				res.writeHead(500);
+				res.end();
+			}
+		}
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` around lines 52
- 55, The Promise.all call that awaits EmojiCustom.countByNameOrAlias and
RocketChatFileEmojiCustomInstance.getFileWithReadStream must be wrapped in a
try/catch inside the WebApp.connectHandlers async handler to avoid unhandled
rejections; catch the error, log it (using the existing logger or
console.error), send an HTTP 500 response (res.writeHead(500) / res.end with a
short message) and ensure the response is closed so the client doesn't hang,
then return early. Locate the await Promise.all([...]) containing
EmojiCustom.countByNameOrAlias and
RocketChatFileEmojiCustomInstance.getFileWithReadStream and wrap it with this
error handling.
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js (1)

51-54: Add a try/catch to handle DB/IO errors in the async handler.

CustomSounds.findOneById is a new async DB operation; if it (or getFileWithReadStream) throws, the unhandled rejection from the async middleware handler leaves the response hanging for the client.

♻️ Proposed fix
 	return WebApp.connectHandlers.use('/custom-sounds/', async (req, res /* , next*/) => {
+		try {
 		const fileId = decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, ''));
 		// ... rest of handler ...
 		file.readStream.pipe(res);
+		} catch (err) {
+			SystemLogger.error({ msg: 'Error serving custom-sound', err });
+			if (!res.headersSent) {
+				res.writeHead(500);
+				res.end();
+			}
+		}
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js` around lines
51 - 54, The Promise.all call invoking CustomSounds.findOneById and
RocketChatFileCustomSoundsInstance.getFileWithReadStream can throw and must be
wrapped in a try/catch inside the async request handler; add a try/catch around
the await Promise.all([CustomSounds.findOneById(...),
RocketChatFileCustomSoundsInstance.getFileWithReadStream(...)]) to log the error
(use the existing logger) and return a proper HTTP error response to the client
(e.g., send a 500 with a short message or call the handler's next(err)), so the
request does not hang when fileId DB/IO operations fail.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6ac61 and a232ce2.

📒 Files selected for processing (2)
  • apps/meteor/app/custom-sounds/server/startup/custom-sounds.js
  • apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
  • apps/meteor/app/custom-sounds/server/startup/custom-sounds.js
🧠 Learnings (2)
📓 Common learnings
Learnt from: d-gubert
Repo: RocketChat/Rocket.Chat PR: 37547
File: packages/i18n/src/locales/en.i18n.json:634-634
Timestamp: 2025-11-19T12:32:29.696Z
Learning: Repo: RocketChat/Rocket.Chat
Context: i18n workflow
Learning: In this repository, new translation keys should be added to packages/i18n/src/locales/en.i18n.json only; other locale files are populated via the external translation pipeline and/or fall back to English. Do not request adding the same key to all locale files in future reviews.
Learnt from: MartinSchoeler
Repo: RocketChat/Rocket.Chat PR: 37408
File: apps/meteor/client/views/admin/ABAC/useRoomAttributeOptions.tsx:53-69
Timestamp: 2025-11-10T19:06:20.146Z
Learning: In the Rocket.Chat repository, do not provide suggestions or recommendations about code sections marked with TODO comments. The maintainers have already identified these as future work and external reviewers lack the full context about implementation plans and timing.
📚 Learning: 2026-02-10T16:32:49.806Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 38528
File: apps/meteor/client/startup/roles.ts:14-14
Timestamp: 2026-02-10T16:32:49.806Z
Learning: In Rocket.Chat's Meteor client code, DDP streams use EJSON which automatically deserializes Date objects. Stream handlers (e.g., sdk.stream()) receive Date fields as Date objects directly and do not require manual conversion using `new Date()`. Only REST API responses require manual date conversion because they return plain JSON where dates are serialized as strings.

Applied to files:

  • apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
🧬 Code graph analysis (2)
apps/meteor/app/emoji-custom/server/startup/emoji-custom.js (1)
packages/models/src/index.ts (1)
  • EmojiCustom (148-148)
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js (1)
packages/models/src/index.ts (1)
  • CustomSounds (144-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
apps/meteor/app/custom-sounds/server/startup/custom-sounds.js (1)

51-61: The core security fix (parallel fetch + DB-record guard) looks correct.

CustomSounds.findOneById(fileId.split('.')[0], { projection: { _id: 1 } }) is a lightweight existence check that correctly blocks cross-resource access: a request to /custom-sounds/<emoji-name> will find no CustomSounds record and short-circuit to 404 before the file bytes are streamed. Using file.contentType for the Content-Type header (line 88) is also the right approach — it reflects the actual MIME type regardless of filename extension.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js`:
- Around line 52-59: The concurrent Promise.all creates a live readStream from
RocketChatFileEmojiCustomInstance.getFileWithReadStream even when
EmojiCustom.countByNameOrAlias is zero or when a 304 is returned; ensure you
explicitly destroy/close file.readStream before any early return: in the guard
that checks if (!file || !emojiCount) and in the cache-validation branch that
returns 304, call file.readStream.destroy() (or file.readStream.close() if using
a different stream API) if file and file.readStream exist, then proceed to send
the SVG placeholder or 304 response; reference the functions
EmojiCustom.countByNameOrAlias,
RocketChatFileEmojiCustomInstance.getFileWithReadStream and the file.readStream
object when applying the fix.

---

Outside diff comments:
In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js`:
- Around line 51-61: The current Promise.all opens the file stream via
RocketChatFileCustomSoundsInstance.getFileWithReadStream before verifying DB
absence from CustomSounds.findOneById, which can leak file.readStream when sound
is null; update the handler so that if sound is missing but file is non-null you
call file.readStream.destroy() (or file.readStream.close() if appropriate)
before writing the 404 response and returning, ensuring you reference the same
RocketChatFileCustomSoundsInstance.getFileWithReadStream result and
CustomSounds.findOneById check to locate where to insert the stream cleanup.

---

Nitpick comments:
In `@apps/meteor/app/custom-sounds/server/startup/custom-sounds.js`:
- Around line 51-54: The Promise.all call invoking CustomSounds.findOneById and
RocketChatFileCustomSoundsInstance.getFileWithReadStream can throw and must be
wrapped in a try/catch inside the async request handler; add a try/catch around
the await Promise.all([CustomSounds.findOneById(...),
RocketChatFileCustomSoundsInstance.getFileWithReadStream(...)]) to log the error
(use the existing logger) and return a proper HTTP error response to the client
(e.g., send a 500 with a short message or call the handler's next(err)), so the
request does not hang when fileId DB/IO operations fail.

In `@apps/meteor/app/emoji-custom/server/startup/emoji-custom.js`:
- Around line 104-110: Replace the URL-extension based Content-Type logic in the
emoji endpoint with the stored file's MIME type: read and use the uploaded
record's file.contentType (the same property used in custom-sounds.js) to set
res.setHeader('Content-Type', file.contentType); if file.contentType is missing
or empty, fall back to a sensible default (e.g., 'image/jpeg') to preserve
compatibility; keep references to params.emoji only for lookup, not for deriving
the MIME type.
- Around line 52-53: Replace the count-based existence check by using a
minimal-document find: replace the call to EmojiCustom.countByNameOrAlias(...)
with EmojiCustom.findOneByNameOrAlias(params.emoji.split('.')[0], { projection:
{ _id: 1 } }) (or implement/find an equivalent finder), update the destructured
variable name from emojiCount to something like emojiDoc (or emojiExistsDoc),
and change the boolean check from !emojiCount to !emojiDoc to preserve behavior;
this mirrors the custom-sounds pattern and avoids a full count aggregation.
- Around line 52-55: The Promise.all call that awaits
EmojiCustom.countByNameOrAlias and
RocketChatFileEmojiCustomInstance.getFileWithReadStream must be wrapped in a
try/catch inside the WebApp.connectHandlers async handler to avoid unhandled
rejections; catch the error, log it (using the existing logger or
console.error), send an HTTP 500 response (res.writeHead(500) / res.end with a
short message) and ensure the response is closed so the client doesn't hang,
then return early. Locate the await Promise.all([...]) containing
EmojiCustom.countByNameOrAlias and
RocketChatFileEmojiCustomInstance.getFileWithReadStream and wrap it with this
error handling.

@ggazzo ggazzo removed the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot removed the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo added the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo removed the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot removed the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo added the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo removed the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot removed the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo added the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo removed stat: QA assured Means it has been tested and approved by a company insider labels Feb 24, 2026
@dionisio-bot dionisio-bot bot removed the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@ggazzo ggazzo added the stat: QA assured Means it has been tested and approved by a company insider label Feb 24, 2026
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Feb 24, 2026
@dionisio-bot dionisio-bot bot added this pull request to the merge queue Feb 24, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 24, 2026
@dionisio-bot dionisio-bot bot enabled auto-merge February 24, 2026 13:53
@ggazzo ggazzo dismissed stale reviews from KevLehman and themself via 6269156 February 24, 2026 14:16
@ggazzo ggazzo disabled auto-merge February 24, 2026 14:16
@ggazzo ggazzo merged commit b77fe13 into develop Feb 24, 2026
5 of 7 checks passed
@ggazzo ggazzo deleted the fix/sounds-emojis-get-files branch February 24, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stat: QA assured Means it has been tested and approved by a company insider stat: ready to merge PR tested and approved waiting for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants