mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 00:03:23 +00:00
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:
@@ -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()
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user