From 77b5f64fcb998f1eedc400f305bf7e10719f12fa Mon Sep 17 00:00:00 2001 From: Chehak7 Date: Thu, 10 Sep 2026 23:30:56 +0530 Subject: [PATCH 1/2] fix: encode GCS Content-Disposition filenames Quote and escape filenames stored in Google Cloud Storage metadata, and add an RFC 5987 filename parameter for Unicode names.\n\nAdds regression coverage for issue #42091. --- .changeset/gentle-clouds-quote.md | 5 +++ .../getContentDisposition.spec.ts | 32 +++++++++++++++++++ .../GoogleStorage/getContentDisposition.ts | 21 ++++++++++++ .../file-upload/ufs/GoogleStorage/server.ts | 3 +- 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/gentle-clouds-quote.md create mode 100644 apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts create mode 100644 apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts diff --git a/.changeset/gentle-clouds-quote.md b/.changeset/gentle-clouds-quote.md new file mode 100644 index 0000000000000..434a87ea41d30 --- /dev/null +++ b/.changeset/gentle-clouds-quote.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes Google Cloud Storage uploads failing to load in Chromium-based browsers when filenames contain commas, spaces, or other characters that require a quoted `Content-Disposition` value. diff --git a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts new file mode 100644 index 0000000000000..666f1067ac708 --- /dev/null +++ b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { getContentDisposition } from './getContentDisposition'; + +describe('getContentDisposition', () => { + it('quotes filenames containing commas and spaces', () => { + expect(getContentDisposition('inline', 'thumb-Clipboard - August 21, 2026 11:47 AM.png')).to.equal( + 'inline; filename="thumb-Clipboard - August 21, 2026 11:47 AM.png"', + ); + }); + + it('escapes quotes and backslashes in the quoted filename', () => { + expect(getContentDisposition('attachment', 'report "final"\\copy.pdf')).to.equal( + 'attachment; filename="report \\"final\\"\\\\copy.pdf"', + ); + }); + + it('adds an RFC 5987 filename for Unicode characters', () => { + expect(getContentDisposition('inline', "résumé's (final)*.pdf")).to.equal( + "inline; filename=\"r_sum_'s (final)*.pdf\"; filename*=UTF-8''r%C3%A9sum%C3%A9%27s%20%28final%29%2A.pdf", + ); + }); + + it('replaces control characters before constructing the header value', () => { + expect(getContentDisposition('inline', 'report\r\nX-Test: value.txt')).to.equal('inline; filename="report__X-Test: value.txt"'); + }); + + it('preserves simple ASCII filenames', () => { + expect(getContentDisposition('inline', 'photo.png')).to.equal('inline; filename="photo.png"'); + }); +}); diff --git a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts new file mode 100644 index 0000000000000..55d2e67f7f0e4 --- /dev/null +++ b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts @@ -0,0 +1,21 @@ +const replaceControlCharacters = (value: string): string => + Array.from(value, (character) => { + const codePoint = character.codePointAt(0) ?? 0; + return codePoint < 0x20 || codePoint === 0x7f ? '_' : character; + }).join(''); + +const encodeRFC5987Value = (value: string): string => + encodeURIComponent(value).replace(/['()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`); + +export const getContentDisposition = (disposition: 'inline' | 'attachment', fileName: string): string => { + const safeFileName = replaceControlCharacters(fileName); + const asciiFileName = safeFileName.replace(/[^\x20-\x7E]/g, '_'); + const quotedFileName = asciiFileName.replace(/["\\]/g, (character) => `\\${character}`); + const fallback = `${disposition}; filename="${quotedFileName}"`; + + if (asciiFileName === safeFileName) { + return fallback; + } + + return `${fallback}; filename*=UTF-8''${encodeRFC5987Value(safeFileName)}`; +}; diff --git a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/server.ts b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/server.ts index 945790fca6b3e..e29397973c112 100644 --- a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/server.ts +++ b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/server.ts @@ -4,6 +4,7 @@ import type { IUpload } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { check } from 'meteor/check'; +import { getContentDisposition } from './getContentDisposition'; import { UploadFS } from '../../../../../ufs'; import type { StoreOptions } from '../../../../../ufs/ufs-store'; import { SystemLogger } from '../../../../logger/system'; @@ -149,7 +150,7 @@ class GoogleStorageStore extends UploadFS.Store { gzip: false, metadata: { contentType: file.type, - contentDisposition: `inline; filename=${file.name}`, + contentDisposition: getContentDisposition('inline', file.name || ''), // metadata: { // custom: 'metadata' // } From 5ec5fa2cd0bbd5b52da8560d23aa603ea228aa8d Mon Sep 17 00:00:00 2001 From: Chehak7 Date: Sun, 13 Sep 2026 23:22:36 +0530 Subject: [PATCH 2/2] fix: address Content-Disposition review feedback Sanitize C1 control characters, normalize malformed Unicode before encoding, and clarify remediation for existing GCS objects. --- .changeset/gentle-clouds-quote.md | 2 ++ .../ufs/GoogleStorage/getContentDisposition.spec.ts | 10 ++++++++-- .../ufs/GoogleStorage/getContentDisposition.ts | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.changeset/gentle-clouds-quote.md b/.changeset/gentle-clouds-quote.md index 434a87ea41d30..787a3be01ad8b 100644 --- a/.changeset/gentle-clouds-quote.md +++ b/.changeset/gentle-clouds-quote.md @@ -3,3 +3,5 @@ --- Fixes Google Cloud Storage uploads failing to load in Chromium-based browsers when filenames contain commas, spaces, or other characters that require a quoted `Content-Disposition` value. + +This fix applies to new uploads. Existing affected objects must be re-uploaded or have their `Content-Disposition` metadata repaired manually. diff --git a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts index 666f1067ac708..c1ec176f85b4d 100644 --- a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts +++ b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.spec.ts @@ -22,8 +22,14 @@ describe('getContentDisposition', () => { ); }); - it('replaces control characters before constructing the header value', () => { - expect(getContentDisposition('inline', 'report\r\nX-Test: value.txt')).to.equal('inline; filename="report__X-Test: value.txt"'); + it('replaces C0 and C1 control characters before constructing the header value', () => { + expect(getContentDisposition('inline', 'report\r\n\u0085X-Test: value.txt')).to.equal('inline; filename="report___X-Test: value.txt"'); + }); + + it('normalizes unpaired UTF-16 surrogates before encoding the filename', () => { + expect(getContentDisposition('inline', 'bad\uD800name.txt')).to.equal( + 'inline; filename="bad_name.txt"; filename*=UTF-8\'\'bad%EF%BF%BDname.txt', + ); }); it('preserves simple ASCII filenames', () => { diff --git a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts index 55d2e67f7f0e4..7eef790415a10 100644 --- a/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts +++ b/apps/meteor/server/lib/media/file-upload/ufs/GoogleStorage/getContentDisposition.ts @@ -1,14 +1,14 @@ const replaceControlCharacters = (value: string): string => Array.from(value, (character) => { const codePoint = character.codePointAt(0) ?? 0; - return codePoint < 0x20 || codePoint === 0x7f ? '_' : character; + return codePoint < 0x20 || (codePoint >= 0x7f && codePoint <= 0x9f) ? '_' : character; }).join(''); const encodeRFC5987Value = (value: string): string => encodeURIComponent(value).replace(/['()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`); export const getContentDisposition = (disposition: 'inline' | 'attachment', fileName: string): string => { - const safeFileName = replaceControlCharacters(fileName); + const safeFileName = replaceControlCharacters(fileName.toWellFormed()); const asciiFileName = safeFileName.replace(/[^\x20-\x7E]/g, '_'); const quotedFileName = asciiFileName.replace(/["\\]/g, (character) => `\\${character}`); const fallback = `${disposition}; filename="${quotedFileName}"`;