From 7430afd701f3db36e4a90b808fe5c81f709a4ae0 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Fri, 16 Oct 2026 15:53:30 +0530 Subject: [PATCH 1/3] fix: escape HTML and truncate long messages in Telegram notifications --- .../src/utils/notifications/build-error.ts | 3 +- .../utils/notifications/database-backup.ts | 3 +- .../src/utils/notifications/docker-cleanup.ts | 3 +- .../src/utils/notifications/dokploy-backup.ts | 3 +- .../utils/notifications/server-threshold.ts | 3 +- .../server/src/utils/notifications/utils.ts | 35 ++++++++++++++++++- .../src/utils/notifications/volume-backup.ts | 3 +- 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 7c299809b..51c10232b 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -201,7 +202,7 @@ export const sendBuildErrorNotifications = async ({ await sendTelegramNotification( telegram, - `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
`, inlineButton, ); } diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 7a838ff46..70c111947 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -208,7 +209,7 @@ export const sendDatabaseBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 98d4cec53..38a3cc068 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -143,7 +144,7 @@ export const sendDockerCleanupNotifications = async ( if (telegram) { await sendTelegramNotification( telegram, - `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + `✅ Docker Cleanup\n\nMessage: ${escapeHtml(message)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/dokploy-backup.ts b/packages/server/src/utils/notifications/dokploy-backup.ts index e838534f2..ab5e8a22e 100644 --- a/packages/server/src/utils/notifications/dokploy-backup.ts +++ b/packages/server/src/utils/notifications/dokploy-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -186,7 +187,7 @@ export const sendDokployBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` diff --git a/packages/server/src/utils/notifications/server-threshold.ts b/packages/server/src/utils/notifications/server-threshold.ts index a8749a349..a32217204 100644 --- a/packages/server/src/utils/notifications/server-threshold.ts +++ b/packages/server/src/utils/notifications/server-threshold.ts @@ -4,6 +4,7 @@ import { db } from "../../db"; import { notifications } from "../../db/schema"; import ServerThresholdEmail from "../../emails/emails/server-threshold"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -193,7 +194,7 @@ export const sendServerThresholdNotifications = async ( Type: ${payload.Type} Current Value: ${payload.Value.toFixed(2)}% Threshold: ${payload.Threshold.toFixed(2)}% - Message: ${payload.Message} + Message: ${escapeHtml(payload.Message)} Time: ${date.toLocaleString()} `, ); diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index d65258862..506713352 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -101,6 +101,30 @@ export const sendDiscordNotification = async ( } }; +export const escapeHtml = (s: string) => + s.replace(/&/g, "&").replace(//g, ">"); + +export const formatTelegramErrorMessage = (errorMessage: string) => { + const escaped = escapeHtml(errorMessage); + const limit = 3000; + if (escaped.length <= limit) return escaped; + let end = limit; + const ampIdx = escaped.lastIndexOf("&", end); + if (ampIdx !== -1 && escaped.indexOf(";", ampIdx) > end) { + end = ampIdx; + } + if ( + end > 0 && + end < escaped.length && + isHighSurrogate(escaped.charCodeAt(end - 1)) + ) { + end--; + } + return `${escaped.substring(0, end)}…`; +}; + +const isHighSurrogate = (code: number) => code >= 0xd800 && code <= 0xdbff; + export const sendTelegramNotification = async ( connection: typeof telegram.$inferInsert, messageText: string, @@ -111,7 +135,7 @@ export const sendTelegramNotification = async ( ) => { try { const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; - await fetch(url, { + const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -125,8 +149,17 @@ export const sendTelegramNotification = async ( }, }), }); + if (!response.ok) { + const body = await response.text(); + throw new Error( + `Failed to send telegram notification: ${response.status} ${body}`, + ); + } } catch (err) { console.log(err); + throw new Error( + `Failed to send telegram notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index cdb1e3a96..2eb9c3c5f 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -223,7 +224,7 @@ export const sendVolumeBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` From 3e75dc64f391316df63a99fbef3e0df5bf7b9350 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Fri, 16 Oct 2026 16:04:26 +0530 Subject: [PATCH 2/3] Update packages/server/src/utils/notifications/utils.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/server/src/utils/notifications/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 506713352..a5603d975 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -110,7 +110,7 @@ export const formatTelegramErrorMessage = (errorMessage: string) => { if (escaped.length <= limit) return escaped; let end = limit; const ampIdx = escaped.lastIndexOf("&", end); - if (ampIdx !== -1 && escaped.indexOf(";", ampIdx) > end) { + if (ampIdx !== -1 && escaped.indexOf(";", ampIdx) >= end) { end = ampIdx; } if ( From 41cecde7353d837240af659e0419f45f9f4e0199 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Fri, 16 Oct 2026 16:09:00 +0530 Subject: [PATCH 3/3] fix: escape metadata fields and fix entity boundary off-by-one --- packages/server/src/utils/notifications/build-error.ts | 3 ++- packages/server/src/utils/notifications/build-success.ts | 3 ++- packages/server/src/utils/notifications/database-backup.ts | 3 ++- packages/server/src/utils/notifications/dokploy-backup.ts | 3 ++- packages/server/src/utils/notifications/server-threshold.ts | 2 +- packages/server/src/utils/notifications/volume-backup.ts | 5 +++-- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 51c10232b..465a7c006 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, @@ -202,7 +203,7 @@ export const sendBuildErrorNotifications = async ({ await sendTelegramNotification( telegram, - `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
`, + `⚠️ Build Failed\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nType: ${escapeHtml(applicationType)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
`, inlineButton, ); } diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 13ed68a77..658acdc9b 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -6,6 +6,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -214,7 +215,7 @@ export const sendBuildSuccessNotifications = async ({ await sendTelegramNotification( telegram, - `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format( + `✅ Build Success\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nEnvironment: ${escapeHtml(environmentName)}\nType: ${escapeHtml(applicationType)}\nDate: ${format( date, "PP", )}\nTime: ${format(date, "pp")}`, diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 70c111947..e6b8ffa60 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, @@ -212,7 +213,7 @@ export const sendDatabaseBackupNotifications = async ({ ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; - const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nType: ${escapeHtml(databaseType)}\nDatabase Name: ${escapeHtml(databaseName)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; await sendTelegramNotification(telegram, messageText); } diff --git a/packages/server/src/utils/notifications/dokploy-backup.ts b/packages/server/src/utils/notifications/dokploy-backup.ts index ab5e8a22e..07048c8df 100644 --- a/packages/server/src/utils/notifications/dokploy-backup.ts +++ b/packages/server/src/utils/notifications/dokploy-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { + escapeHtml, formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, @@ -190,7 +191,7 @@ export const sendDokployBackupNotifications = async ({ ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize - ? `\nBackup Size: ${backupSize}` + ? `\nBackup Size: ${escapeHtml(backupSize)}` : ""; const messageText = `${statusEmoji} Dokploy Backup ${typeStatus}\n\nBackup Type: Complete Dokploy Instance${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; diff --git a/packages/server/src/utils/notifications/server-threshold.ts b/packages/server/src/utils/notifications/server-threshold.ts index a32217204..4e829f79e 100644 --- a/packages/server/src/utils/notifications/server-threshold.ts +++ b/packages/server/src/utils/notifications/server-threshold.ts @@ -190,7 +190,7 @@ export const sendServerThresholdNotifications = async ( telegram, ` ⚠️ Server ${payload.Type} Alert - Server Name: ${payload.ServerName} + Server Name: ${escapeHtml(payload.ServerName)} Type: ${payload.Type} Current Value: ${payload.Value.toFixed(2)}% Threshold: ${payload.Threshold.toFixed(2)}% diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index 2eb9c3c5f..54f9b97a4 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, @@ -227,10 +228,10 @@ export const sendVolumeBackupNotifications = async ({ ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize - ? `\nBackup Size: ${backupSize}` + ? `\nBackup Size: ${escapeHtml(backupSize)}` : ""; - const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nVolume Name: ${escapeHtml(volumeName)}\nService Type: ${escapeHtml(serviceType)}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; await sendTelegramNotification(telegram, messageText); }