From a9c85f846182fa1f83dd06bce181407ddc801bc1 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 6 Feb 2026 19:25:17 -0300 Subject: [PATCH 1/5] avoid getting other files than images from emoji-custom endpoint --- .../app/emoji-custom/server/startup/emoji-custom.js | 10 ++++++---- apps/meteor/server/services/image/service.ts | 4 ++++ packages/core-services/src/types/IMediaService.ts | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index fed5123b115f4..2acd1a7344047 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -1,6 +1,6 @@ +import { Media } from '@rocket.chat/core-services'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; -import _ from 'underscore'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { RocketChatFile } from '../../../file/server'; @@ -41,7 +41,9 @@ Meteor.startup(() => { return WebApp.connectHandlers.use('/emoji-custom/', async (req, res /* , next*/) => { const params = { emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) }; - if (_.isEmpty(params.emoji)) { + const extension = params.emoji?.split('.').pop()?.toLowerCase() ?? ''; + + if (!extension || !(await Media.isImageExtension(extension))) { res.writeHead(403); res.write('Forbidden'); res.end(); @@ -97,9 +99,9 @@ Meteor.startup(() => { res.setHeader('Last-Modified', fileUploadDate || new Date().toUTCString()); res.setHeader('Content-Length', file.length); - if (/^svg$/i.test(params.emoji.split('.').pop())) { + if (/^svg$/i.test(extension)) { res.setHeader('Content-Type', 'image/svg+xml'); - } else if (/^png$/i.test(params.emoji.split('.').pop())) { + } else if (/^png$/i.test(extension)) { res.setHeader('Content-Type', 'image/png'); } else { res.setHeader('Content-Type', 'image/jpeg'); diff --git a/apps/meteor/server/services/image/service.ts b/apps/meteor/server/services/image/service.ts index 85b4d2d05f979..1c7d60e613f02 100644 --- a/apps/meteor/server/services/image/service.ts +++ b/apps/meteor/server/services/image/service.ts @@ -104,4 +104,8 @@ export class MediaService extends ServiceClassInternal implements IMediaService bufferStream.end(buffer); return bufferStream; } + + isImageExtension(extension: string): boolean { + return this.imageExts.has(extension); + } } diff --git a/packages/core-services/src/types/IMediaService.ts b/packages/core-services/src/types/IMediaService.ts index f155e360e0eb1..663f20745407d 100644 --- a/packages/core-services/src/types/IMediaService.ts +++ b/packages/core-services/src/types/IMediaService.ts @@ -30,4 +30,5 @@ export interface IMediaService { isImage(buff: Buffer): Promise; stripExifFromImageStream(stream: Stream): Readable; stripExifFromBuffer(buffer: Buffer): Promise; + isImageExtension(extension: string): boolean; } From 854ceb9ef3351034bfdab510613940d21978ffcd Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Sat, 7 Feb 2026 01:01:51 -0300 Subject: [PATCH 2/5] use contentType property to validate file fetching --- .../server/startup/custom-sounds.js | 3 +- .../server/startup/emoji-custom.js | 5 ++- apps/meteor/server/services/image/service.ts | 4 --- .../tests/end-to-end/api/emoji-custom.ts | 2 +- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++++++++++----- .../core-services/src/types/IMediaService.ts | 1 - 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js index 117a7d3c9e759..d9a839e3a3b82 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js @@ -48,7 +48,8 @@ Meteor.startup(() => { } const file = await RocketChatFileCustomSoundsInstance.getFileWithReadStream(fileId); - if (!file) { + + if (!file || !file.contentType?.startsWith('audio/')) { res.writeHead(404); res.write('Not found'); res.end(); diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index 2acd1a7344047..b98f4dc542382 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -1,4 +1,3 @@ -import { Media } from '@rocket.chat/core-services'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; @@ -43,7 +42,7 @@ Meteor.startup(() => { const extension = params.emoji?.split('.').pop()?.toLowerCase() ?? ''; - if (!extension || !(await Media.isImageExtension(extension))) { + if (!extension) { res.writeHead(403); res.write('Forbidden'); res.end(); @@ -54,7 +53,7 @@ Meteor.startup(() => { res.setHeader('Content-Disposition', 'inline'); - if (!file) { + if (!file || !file.contentType?.startsWith('image/')) { // use code from username initials renderer until file upload is complete res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=0'); diff --git a/apps/meteor/server/services/image/service.ts b/apps/meteor/server/services/image/service.ts index 1c7d60e613f02..85b4d2d05f979 100644 --- a/apps/meteor/server/services/image/service.ts +++ b/apps/meteor/server/services/image/service.ts @@ -104,8 +104,4 @@ export class MediaService extends ServiceClassInternal implements IMediaService bufferStream.end(buffer); return bufferStream; } - - isImageExtension(extension: string): boolean { - return this.imageExts.has(extension); - } } diff --git a/apps/meteor/tests/end-to-end/api/emoji-custom.ts b/apps/meteor/tests/end-to-end/api/emoji-custom.ts index 744480ea6f716..b04baa5183073 100644 --- a/apps/meteor/tests/end-to-end/api/emoji-custom.ts +++ b/apps/meteor/tests/end-to-end/api/emoji-custom.ts @@ -5,7 +5,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; -describe('[EmojiCustom]', () => { +describe.only('[EmojiCustom]', () => { const customEmojiName = `my-custom-emoji-${Date.now()}`; let withoutAliases: IEmojiCustom; diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 6dbfd05882fe2..9923094073ef1 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,14 +16,9 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": { - "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" - }, - "@std/cli@1.0.13": { - "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" - }, + "@std/bytes@1.0.6": {}, + "@std/cli@1.0.13": {}, "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -108,7 +103,28 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" + "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", + "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", + "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", + "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", + "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", + "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", + "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", + "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", + "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", + "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", + "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", + "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", + "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", + "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", + "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", + "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", + "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", + "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", + "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", + "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", + "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" }, "workspace": { "dependencies": [ diff --git a/packages/core-services/src/types/IMediaService.ts b/packages/core-services/src/types/IMediaService.ts index 663f20745407d..f155e360e0eb1 100644 --- a/packages/core-services/src/types/IMediaService.ts +++ b/packages/core-services/src/types/IMediaService.ts @@ -30,5 +30,4 @@ export interface IMediaService { isImage(buff: Buffer): Promise; stripExifFromImageStream(stream: Stream): Readable; stripExifFromBuffer(buffer: Buffer): Promise; - isImageExtension(extension: string): boolean; } From 08a694af52563ceae026c95666c11ef0e4f228bc Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Sat, 7 Feb 2026 01:03:40 -0300 Subject: [PATCH 3/5] restore deno.lock --- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 9923094073ef1..6dbfd05882fe2 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,9 +16,14 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": {}, - "@std/cli@1.0.13": {}, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cli@1.0.13": { + "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" + }, "@std/streams@1.0.16": { + "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -103,28 +108,7 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", - "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", - "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", - "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", - "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", - "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", - "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", - "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", - "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", - "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", - "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", - "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", - "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", - "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", - "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", - "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", - "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", - "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", - "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", - "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", - "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" }, "workspace": { "dependencies": [ From 81954f66ccd1d8620ed18fcc571dcd52644861f5 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 9 Feb 2026 14:30:04 -0300 Subject: [PATCH 4/5] remove undesired .only --- apps/meteor/tests/end-to-end/api/emoji-custom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/emoji-custom.ts b/apps/meteor/tests/end-to-end/api/emoji-custom.ts index b04baa5183073..744480ea6f716 100644 --- a/apps/meteor/tests/end-to-end/api/emoji-custom.ts +++ b/apps/meteor/tests/end-to-end/api/emoji-custom.ts @@ -5,7 +5,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; -describe.only('[EmojiCustom]', () => { +describe('[EmojiCustom]', () => { const customEmojiName = `my-custom-emoji-${Date.now()}`; let withoutAliases: IEmojiCustom; From a41d01c0471a9abea5dda75d694a7dbd621e40cb Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 9 Feb 2026 14:37:30 -0300 Subject: [PATCH 5/5] add changeset --- .changeset/clean-ears-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-ears-fly.md diff --git a/.changeset/clean-ears-fly.md b/.changeset/clean-ears-fly.md new file mode 100644 index 0000000000000..781a8a4da4466 --- /dev/null +++ b/.changeset/clean-ears-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a cross-resource access issue that allowed users to retrieve emojis from the Custom Sounds endpoint and sounds from the Custom Emojis endpoint when using the FileSystem storage mode.