Every SMTP message is just a block of text with headers separated from the body by a blank line, and headers separated from each other by a carriage-return line feed — \r\n. That two-byte sequence is the entire security boundary, and for over a decade it has been the boundary attackers exploit. When application code concatenates user input directly into a "From" name, a subject line, or a reply-to field, an attacker who slips \r\n into that input can inject brand-new headers — a Bcc: line to silently copy a spam run, an extra To: to redirect a password-reset email — or terminate the headers early and write their own message body. PHPMailer, the most widely deployed PHP mail library, carries the clearest paper trail of what this looks like in production: CVE-2012-0796 (header injection via crafted input), CVE-2015-8476 (CRLF injection permitting arbitrary message sending, fixed in 5.2.14), and CVE-2016-10033, where a backslash-quote in a crafted From address escaped into the arguments passed to the underlying sendmail binary — turning a header-injection bug class into full remote code execution, complete with a public proof-of-concept exploit. This piece breaks down the mechanism and the concrete fixes that stop it.
What exactly is SMTP header injection?
SMTP header injection happens when an application builds raw email headers by concatenating strings that include unsanitized user input, and that input contains a CRLF sequence. Because \r\n is the literal delimiter the SMTP protocol and MTAs (mail transfer agents) use to separate one header from the next, an attacker who controls a field like "your name" on a contact form can submit a value such as Attacker\r\nBcc: victim@example.com. If the application does "From: " + name + "\r\n" without checking for embedded line breaks, the resulting message now carries a header the developer never intended to send. Submit two consecutive CRLFs and the attacker terminates the header block entirely, writing a fabricated message body — turning a contact form into an open spam relay. This is the email-specific case of the broader CRLF-injection pattern also seen in HTTP response splitting.
What did the PHPMailer CVEs actually show?
They showed the bug class escalating from spam-relay abuse to remote code execution over a four-year span. CVE-2012-0796 and CVE-2015-8476 (patched in PHPMailer 5.2.14, November 2015) were classic header-injection flaws: insufficiently sanitized input let an attacker append arbitrary headers or additional recipients. CVE-2016-10033, affecting versions before 5.2.18, was more severe — PHPMailer used PHP's mail() function, which under sendmail_path configurations passes the sender address as a command-line argument to the sendmail binary. A crafted From address containing an escaped shell metacharacter (a backslash-quote) broke out of the intended argument and injected an attacker-controlled option into the sendmail invocation, achieving code execution on the server. A public working exploit (opsxcq/exploit-CVE-2016-10033) demonstrated the full chain, and it remains one of the most cited examples of why "just an email form" can be a critical vulnerability.
Why does PHP's mail() function make this worse?
PHP's mail() function makes this worse because its fifth parameter (additional_params) is passed through to the underlying MTA with only shell-command escaping, not argument-level sanitization, and any library wrapping it inherits that risk unless it adds its own defenses. Sonar's security research team has documented mail() as inherently dangerous precisely because of this parameter-passing behavior: the function was designed decades ago around invoking a local sendmail binary directly, not around handling adversarial, attacker-supplied strings safely. PHPMailer's own SECURITY.md acknowledges this history, and the project's current source actively rejects header-breaking input in its address validation rather than relying on the caller to sanitize it first. The lesson generalizes past PHP: any language or framework where an email helper builds a shell command or a raw header string from user input — rather than calling a structured, validated API — carries the same latent risk, whether or not a public CVE has been filed against it yet.
How do you actually sanitize input for email headers?
You sanitize by rejecting the delimiter itself, not by trying to escape it. Any value destined for a header field — display name, subject, reply-to — should be checked for CR (\r) and LF (\n) characters, including their URL-encoded forms %0d and %0a if the input arrives via a query string, and rejected or stripped outright rather than passed through. For email addresses specifically, validate against the RFC 5321/5322 address grammar (a well-tested regex or your language's email-parsing library) instead of blocklisting individual characters, since blocklists are routinely bypassed by encoding or Unicode tricks. Modern mail libraries have moved this defense into the library itself: current PHPMailer, Symfony Mailer (successor to SwiftMailer), and Node's Nodemailer all reject or strip embedded CRLF sequences by design when you use their structured setters (setFrom(), addBCC(), Subject) instead of building header strings by hand. The fix is as much "use the library correctly" as it is "write a regex."
What does defense in depth look like beyond input validation?
Defense in depth here means treating the mail library as the sole path to the wire, keeping it current, and constraining what user-controlled fields are allowed to become. Never construct raw MIME headers via string concatenation, even for "trusted" internal use — a display name field that's safe today becomes a source the moment marketing adds a "share via email" feature. Pin and patch your mail library; the PHPMailer CVE history exists precisely because applications kept running 5.2.17 and earlier long after 5.2.18 shipped. Where a user controls a Reply-To or From domain, allow-list acceptable domains instead of accepting anything RFC-valid, since a syntactically correct address can still be an attacker's own mailbox used for phishing replies. And log rejected header-injection attempts — a spike in stripped CRLF sequences on a contact form is a reliable early signal of automated spam-relay probing.
How Safeguard helps
Safeguard's SAST engine traces untrusted input from source to sink across a codebase, and header-construction calls that concatenate a request parameter into a mail header are exactly the kind of source-to-sink path it's built to surface — the finding carries the full dataflow trace from the HTTP input to the vulnerable mail() or header-string call, with a CWE mapping, so a reviewer sees why it was flagged rather than just a bare line number. That matters for injection classes like this one, where the dangerous call itself (a mail library invocation) looks completely benign in isolation and only becomes a finding once you know it's fed by unsanitized user input. Pairing that reachability context with dependency scanning also closes the other half of the gap: teams still shipping a pre-5.2.18 PHPMailer, or an equivalent unpatched mail library in another language, get flagged as an exploitable, known-CVE dependency rather than a silent risk waiting for the next disclosure.