From 2d7fc74c51f9d85ec16bb77d72d5451f61d78e15 Mon Sep 17 00:00:00 2001 From: sam28u Date: Tue, 9 Jun 2026 22:43:09 +0530 Subject: [PATCH 1/4] fix(chat): enforce soft-delete checks and sanitize invisible characters in messages --- apps/meteor/app/api/server/v1/chat.ts | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 383f4312c53b5..4fd3eaada3316 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -268,6 +268,22 @@ const chatEndpoints = API.v1 return API.v1.failure(`No message found with the id of "${bodyParams.msgId}".`); } + // Prevent editing of soft-deleted messages (Zombie Edits fix) + if (msg.t === 'rm') { + return API.v1.failure('Cannot edit a deleted message.'); + } + + // --- Fix : Strict Input Sanitization for updates --- + if ('text' in bodyParams && typeof bodyParams.text === 'string') { + const sanitizedText = bodyParams.text.replace(/\u0000/g, '').trim(); + + if (sanitizedText.length === 0) { + return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); + } + + bodyParams.text = bodyParams.text.replace(/\u0000/g, ''); + } + if (bodyParams.roomId !== msg.rid) { return API.v1.failure('The room id provided does not match where the message is from.'); } @@ -837,6 +853,21 @@ const chatEndpoints = API.v1 throw new Error("Cannot send system messages using 'chat.sendMessage'"); } + // --- Fix : Strict Input Sanitization (Invisible Character Bypass) --- + const msgPayload = this.bodyParams.message as { msg?: string; attachments?: any[] }; + if (msgPayload && typeof msgPayload.msg === 'string') { + // Strip null bytes and trim + const sanitizedMsg = msgPayload.msg.replace(/\u0000/g, '').trim(); + + // If the message is completely empty after stripping, and has no attachments, reject it + if (sanitizedMsg.length === 0 && (!msgPayload.attachments || msgPayload.attachments.length === 0)) { + return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); + } + + // Clean the actual payload sent to the database + msgPayload.msg = msgPayload.msg.replace(/\u0000/g, ''); + } + const sent = await applyAirGappedRestrictionsValidation(() => executeSendMessage(this.user, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), ); From add18dadab52fc23da3b3e831b80983d91247aaa Mon Sep 17 00:00:00 2001 From: sam28u Date: Tue, 9 Jun 2026 23:03:33 +0530 Subject: [PATCH 2/4] fix(chat): update regex to catch all control chars and add changeset --- apps/meteor/app/api/server/v1/chat.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 4fd3eaada3316..c2f3be6997eb2 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -275,13 +275,13 @@ const chatEndpoints = API.v1 // --- Fix : Strict Input Sanitization for updates --- if ('text' in bodyParams && typeof bodyParams.text === 'string') { - const sanitizedText = bodyParams.text.replace(/\u0000/g, '').trim(); + const sanitizedText = bodyParams.text.replace(/[\p{Cc}]/gu, '').trim(); if (sanitizedText.length === 0) { return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); } - bodyParams.text = bodyParams.text.replace(/\u0000/g, ''); + bodyParams.text = bodyParams.text.replace(/[\p{Cc}]/gu, ''); } if (bodyParams.roomId !== msg.rid) { @@ -857,7 +857,7 @@ const chatEndpoints = API.v1 const msgPayload = this.bodyParams.message as { msg?: string; attachments?: any[] }; if (msgPayload && typeof msgPayload.msg === 'string') { // Strip null bytes and trim - const sanitizedMsg = msgPayload.msg.replace(/\u0000/g, '').trim(); + const sanitizedMsg = msgPayload.msg.replace(/[\p{Cc}]/gu, '').trim(); // If the message is completely empty after stripping, and has no attachments, reject it if (sanitizedMsg.length === 0 && (!msgPayload.attachments || msgPayload.attachments.length === 0)) { @@ -865,7 +865,7 @@ const chatEndpoints = API.v1 } // Clean the actual payload sent to the database - msgPayload.msg = msgPayload.msg.replace(/\u0000/g, ''); + msgPayload.msg = msgPayload.msg.replace(/[\p{Cc}]/gu, ''); } const sent = await applyAirGappedRestrictionsValidation(() => From 2194b2cb936d2eed75b2378db62eea73f7f11440 Mon Sep 17 00:00:00 2001 From: sam28u Date: Tue, 9 Jun 2026 23:06:35 +0530 Subject: [PATCH 3/4] fix(chat): update regex to catch all control chars and add changeset --- apps/meteor/app/api/server/v1/chat.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index c2f3be6997eb2..537e2bd627500 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -268,12 +268,12 @@ const chatEndpoints = API.v1 return API.v1.failure(`No message found with the id of "${bodyParams.msgId}".`); } - // Prevent editing of soft-deleted messages (Zombie Edits fix) + // Reject modifications to soft-deleted messages if (msg.t === 'rm') { return API.v1.failure('Cannot edit a deleted message.'); } - // --- Fix : Strict Input Sanitization for updates --- + // Sanitize invisible control characters to prevent empty message bypass if ('text' in bodyParams && typeof bodyParams.text === 'string') { const sanitizedText = bodyParams.text.replace(/[\p{Cc}]/gu, '').trim(); @@ -853,18 +853,16 @@ const chatEndpoints = API.v1 throw new Error("Cannot send system messages using 'chat.sendMessage'"); } - // --- Fix : Strict Input Sanitization (Invisible Character Bypass) --- + // Sanitize invisible control characters to prevent empty message bypass const msgPayload = this.bodyParams.message as { msg?: string; attachments?: any[] }; if (msgPayload && typeof msgPayload.msg === 'string') { - // Strip null bytes and trim const sanitizedMsg = msgPayload.msg.replace(/[\p{Cc}]/gu, '').trim(); - // If the message is completely empty after stripping, and has no attachments, reject it + // Reject if the message is empty and has no attachments if (sanitizedMsg.length === 0 && (!msgPayload.attachments || msgPayload.attachments.length === 0)) { return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); } - // Clean the actual payload sent to the database msgPayload.msg = msgPayload.msg.replace(/[\p{Cc}]/gu, ''); } From dddeb0cee67d968e063742f09acbc8e2de2781a0 Mon Sep 17 00:00:00 2001 From: sam28u Date: Tue, 9 Jun 2026 23:30:14 +0530 Subject: [PATCH 4/4] fix(chat): refine control character regex to preserve safe whitespace --- apps/meteor/app/api/server/v1/chat.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 537e2bd627500..ea26af63222e9 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -273,15 +273,15 @@ const chatEndpoints = API.v1 return API.v1.failure('Cannot edit a deleted message.'); } - // Sanitize invisible control characters to prevent empty message bypass + // Sanitize invisible control characters (excluding safe whitespace) to prevent empty message bypass if ('text' in bodyParams && typeof bodyParams.text === 'string') { - const sanitizedText = bodyParams.text.replace(/[\p{Cc}]/gu, '').trim(); + const sanitizedText = bodyParams.text.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '').trim(); if (sanitizedText.length === 0) { return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); } - bodyParams.text = bodyParams.text.replace(/[\p{Cc}]/gu, ''); + bodyParams.text = bodyParams.text.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ''); } if (bodyParams.roomId !== msg.rid) { @@ -853,17 +853,17 @@ const chatEndpoints = API.v1 throw new Error("Cannot send system messages using 'chat.sendMessage'"); } - // Sanitize invisible control characters to prevent empty message bypass + // Sanitize invisible control characters (excluding safe whitespace) to prevent empty message bypass const msgPayload = this.bodyParams.message as { msg?: string; attachments?: any[] }; if (msgPayload && typeof msgPayload.msg === 'string') { - const sanitizedMsg = msgPayload.msg.replace(/[\p{Cc}]/gu, '').trim(); + const sanitizedMsg = msgPayload.msg.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '').trim(); // Reject if the message is empty and has no attachments if (sanitizedMsg.length === 0 && (!msgPayload.attachments || msgPayload.attachments.length === 0)) { return API.v1.failure('Message cannot be empty or contain only invisible control characters.'); } - msgPayload.msg = msgPayload.msg.replace(/[\p{Cc}]/gu, ''); + msgPayload.msg = msgPayload.msg.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ''); } const sent = await applyAirGappedRestrictionsValidation(() =>