Email header injection exploits the CRLF-based structure of the SMTP protocol. An attacker who can control a merge field value — say, a user's first name registered as Alice\r\nBcc: victim@example.com — can inject additional headers into outgoing emails, silently copying sends to arbitrary recipients. This maps to CWE-93 (CRLF injection) and OWASP A03 (Injection). Subject lines and Reply-To addresses built from unsanitized user input are the most common injection surfaces. The Compliance & Consent Engine category verifies that unsubscribe mechanisms exist — header injection is the vector by which those mechanisms can be bypassed.
Critical because CRLF injection in email headers allows an attacker to redirect outbound email to unintended recipients without any access control bypass.
Strip CR, LF, and null bytes from all user-controlled values placed in headers, and enforce a length cap, in lib/email/sanitize.ts:
export function sanitizeHeader(value: string): string {
return value.replace(/[\r\n\0]/g, ' ').trim().slice(0, 256)
}
export function sanitizeEmailAddress(address: string): string {
const clean = address.replace(/[\r\n\0<>]/g, '').trim()
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(clean)) {
throw new Error(`Invalid email address: ${clean}`)
}
return clean
}
Apply sanitizeHeader to every field that accepts user input before the value is passed to the ESP SDK — at minimum: subject, from name, and reply-to address.
ID: sending-pipeline-infrastructure.template-engine.merge-field-sanitization
Severity: critical
What to look for: Examine how personalization data (first name, company name, custom fields) is merged into email content. Specifically, check for any personalization data that is inserted into email headers (Subject line, From name, Reply-To) without sanitization. Email header injection allows an attacker who can control the subject or from-name to inject additional headers (Bcc, Cc) that cause the email to be sent to unintended recipients. Look for raw user input inserted into subject-line templates, from-name strings, or reply-to addresses.
Pass criteria: User-controlled personalization data inserted into email headers (subject, from, reply-to) is sanitized to strip newline characters (\r, \n) and other control characters before being rendered. Subject and header fields have a maximum length enforced at no more than 256 characters. Enumerate all header fields that accept user input and count the number that are sanitized — all must be covered. Do NOT pass when sanitization exists on subject but not on reply-to or from-name.
Fail criteria: User input is directly interpolated into the email subject or from/reply-to fields without stripping newline characters. An attacker who can control a merge field value could inject additional mail headers.
Skip (N/A) when: The application sends only fixed-template emails with no user-controlled content in headers — confirmed by reviewing all template rendering code.
Cross-reference: The Compliance & Consent Engine Audit verifies that unsubscribe mechanisms are present — this check verifies that the template rendering layer cannot be exploited to bypass those mechanisms via header injection.
Detail on fail: "First name merge field interpolated directly into Subject header with no newline stripping — header injection possible if user name contains \\r\\n" or "Reply-To header built from user-supplied email address without validation or sanitization"
Remediation: Strip newline characters from any user data placed in headers, and validate header values:
// lib/email/sanitize.ts
export function sanitizeHeader(value: string): string {
// Remove CR, LF, and null bytes that could inject additional headers
return value
.replace(/[\r\n\0]/g, ' ')
.trim()
.slice(0, 256) // Enforce reasonable max length
}
export function sanitizeEmailAddress(address: string): string {
// Validate format — reject anything with newlines or unexpected characters
const clean = address.replace(/[\r\n\0<>]/g, '').trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(clean)) {
throw new Error(`Invalid email address: ${clean}`)
}
return clean
}
// In your template rendering:
const subject = sanitizeHeader(`Hello ${mergeFields.firstName}, your order is ready`)
const replyTo = sanitizeEmailAddress(mergeFields.replyToEmail)