From c7db65980a8b106e136f5e0213cca6fe32bba7f1 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Sep 2025 00:07:53 +0200 Subject: [PATCH] Fix security vulnerabilities in fromName field handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add sanitizeDisplayName() method to prevent header injection attacks - Remove newlines, carriage returns, and control characters from display names - Fix quote escaping inconsistency between getDefaultFrom() and processEmailItem() - Create formatEmailAddress() helper method for consistent email formatting - Add fromName sanitization in sendEmail() function for input validation - Prevent malformed email headers and potential security issues Security improvements: - Header injection prevention (removes \r\n and control characters) - Consistent quote escaping across all display name usage - Proper sanitization at both input and output stages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/sendEmail.ts | 11 +++++++++ src/services/MailingService.ts | 42 ++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 91cca46..12862a4 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -100,6 +100,17 @@ export const sendEmail = async 0 ? validated[0] : undefined } + // Sanitize fromName to prevent header injection + if (emailData.fromName && emailData.fromName !== null) { + emailData.fromName = emailData.fromName + .trim() + // Remove/replace newlines and carriage returns to prevent header injection + .replace(/[\r\n]/g, ' ') + // Remove control characters (except space and printable characters) + .replace(/[\x00-\x1F\x7F-\x9F]/g, '') + // Note: We don't escape quotes here as that's handled in MailingService + } + // Normalize Date objects to ISO strings for consistent database storage if (emailData.scheduledAt instanceof Date) { emailData.scheduledAt = emailData.scheduledAt.toISOString() diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts index a41d9b6..b88b9a6 100644 --- a/src/services/MailingService.ts +++ b/src/services/MailingService.ts @@ -63,15 +63,39 @@ export class MailingService implements IMailingService { } } + /** + * Sanitizes a display name for use in email headers to prevent header injection + * and ensure proper formatting + */ + private sanitizeDisplayName(name: string): string { + return name + .trim() + // Remove/replace newlines and carriage returns to prevent header injection + .replace(/[\r\n]/g, ' ') + // Remove control characters (except space and printable characters) + .replace(/[\x00-\x1F\x7F-\x9F]/g, '') + // Escape quotes to prevent malformed headers + .replace(/"/g, '\\"') + } + + /** + * Formats an email address with optional display name + */ + private formatEmailAddress(email: string, displayName?: string | null): string { + if (displayName && displayName.trim()) { + const sanitizedName = this.sanitizeDisplayName(displayName) + return `"${sanitizedName}" <${email}>` + } + return email + } + private getDefaultFrom(): string { const fromEmail = this.config.defaultFrom const fromName = this.config.defaultFromName // Check if fromName exists, is not empty after trimming, and fromEmail exists if (fromName && fromName.trim() && fromEmail) { - // Escape quotes in the display name to prevent malformed headers - const escapedName = fromName.replace(/"/g, '\\"') - return `"${escapedName}" <${fromEmail}>` + return this.formatEmailAddress(fromEmail, fromName) } return fromEmail || '' @@ -238,12 +262,12 @@ export class MailingService implements IMailingService { id: emailId, }) as BaseEmailDocument - // Combine from and fromName for nodemailer - let fromField = email.from - if (email.fromName && email.from) { - fromField = `"${email.fromName}" <${email.from}>` - } else if (email.from) { - fromField = email.from + // Combine from and fromName for nodemailer using proper sanitization + let fromField: string + if (email.from) { + fromField = this.formatEmailAddress(email.from, email.fromName) + } else { + fromField = this.getDefaultFrom() } const mailOptions = {