Fix security vulnerabilities in fromName field handling

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-09-14 00:07:53 +02:00
parent 624dc12471
commit c7db65980a
2 changed files with 44 additions and 9 deletions

View File

@@ -100,6 +100,17 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
emailData.from = validated && validated.length > 0 ? validated[0] : undefined emailData.from = validated && validated.length > 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 // Normalize Date objects to ISO strings for consistent database storage
if (emailData.scheduledAt instanceof Date) { if (emailData.scheduledAt instanceof Date) {
emailData.scheduledAt = emailData.scheduledAt.toISOString() emailData.scheduledAt = emailData.scheduledAt.toISOString()

View File

@@ -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 { private getDefaultFrom(): string {
const fromEmail = this.config.defaultFrom const fromEmail = this.config.defaultFrom
const fromName = this.config.defaultFromName const fromName = this.config.defaultFromName
// Check if fromName exists, is not empty after trimming, and fromEmail exists // Check if fromName exists, is not empty after trimming, and fromEmail exists
if (fromName && fromName.trim() && fromEmail) { if (fromName && fromName.trim() && fromEmail) {
// Escape quotes in the display name to prevent malformed headers return this.formatEmailAddress(fromEmail, fromName)
const escapedName = fromName.replace(/"/g, '\\"')
return `"${escapedName}" <${fromEmail}>`
} }
return fromEmail || '' return fromEmail || ''
@@ -238,12 +262,12 @@ export class MailingService implements IMailingService {
id: emailId, id: emailId,
}) as BaseEmailDocument }) as BaseEmailDocument
// Combine from and fromName for nodemailer // Combine from and fromName for nodemailer using proper sanitization
let fromField = email.from let fromField: string
if (email.fromName && email.from) { if (email.from) {
fromField = `"${email.fromName}" <${email.from}>` fromField = this.formatEmailAddress(email.from, email.fromName)
} else if (email.from) { } else {
fromField = email.from fromField = this.getDefaultFrom()
} }
const mailOptions = { const mailOptions = {