Protect PHP Mail Forms from Direct Access and Email Injection

Your PHP contact form might be quietly forwarding spam to thousands of addresses. Here's how email header injection works, and how to close the door.

In 2016, a critical vulnerability in PHPMailer (CVE-2016-10033) allowed attackers to execute arbitrary code on any server running a PHP contact form with the library. Millions of WordPress sites were affected. The patch landed within 48 hours, but many sites were never updated.

That was a high-profile case. The quieter version — email header injection — has been running on unprotected PHP forms for over two decades, and it still works on a large percentage of contact forms deployed today.

What Email Header Injection Actually Is

PHP’s built-in mail() function constructs an email by building raw headers. A basic contact form might do this:

$name    = $_POST['name'];
$email   = $_POST['email'];
$message = $_POST['message'];

mail(
  "[email protected]",
  "New contact: " . $name,
  $message,
  "From: " . $email
);

This looks reasonable. The problem is that email headers are separated by newline characters (\r\n). If an attacker submits this as their email address:

[email protected]%0ACc:[email protected]%0ABcc:[email protected],[email protected]

PHP dutifully constructs headers that include those Cc and Bcc lines. Your server just became a spam relay, sending your emails to hundreds or thousands of addresses the attacker controls — all from your domain’s sending reputation.

This isn’t theoretical. Automated scanners probe for unprotected mail() calls constantly. A form that’s been live for more than a few months has almost certainly been tested.

The Three Attack Vectors

1. Header Injection via the From Field

The most common attack. The From header is user-supplied in most forms, making it the obvious injection point. Any field that feeds into headers (From, Reply-To, CC) is vulnerable if unsanitized.

2. Subject Line Injection

Less common but equally dangerous. If the form’s subject line is user-controlled:

New message%0ABcc:[email protected],[email protected]

The %0A (URL-encoded newline) splits the header and adds a BCC field.

3. Direct Form Access

A subtler problem: your form’s PHP endpoint is publicly accessible via direct URL. Attackers don’t need to find your form on your website — they can POST directly to yourdomain.com/contact.php with crafted payloads. No browser required, no referrer check, no rate limiting unless you’ve added it explicitly.

Five Things to Fix Right Now

1. Strip Newlines from All Header-Bound Inputs

Carriage returns and line feeds have no legitimate use in email addresses, names, or subject lines. Strip them before they get anywhere near mail():

function sanitize_header($input) {
    return str_replace(["\r", "\n", "%0a", "%0d", "%0A", "%0D"], '', trim($input));
}

$name    = sanitize_header($_POST['name']);
$email   = sanitize_header($_POST['email']);
$subject = sanitize_header($_POST['subject']);

This is the minimum. Do this for every input that could appear in an email header.

2. Validate Email Addresses Strictly

Don’t just strip newlines — validate that the email is actually an email address:

$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
    http_response_code(400);
    exit('Invalid email address');
}

FILTER_VALIDATE_EMAIL rejects strings with newlines or unexpected characters. It’s not perfect, but it’s a fast and reliable first pass.

3. Add a CSRF Token

Direct POST attacks — where bots bypass the form and hit the endpoint directly — require a server-side token tied to the session:

// On form load
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

// On submission
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
    http_response_code(403);
    exit('Invalid request');
}

This stops direct access attacks cold. The token must match what was issued in the same session, and bots that POST directly without visiting the form page won’t have it.

4. Check the Referer Header (Defense in Depth)

Not a primary defense (Referer can be spoofed), but adds a layer:

$referer = $_SERVER['HTTP_REFERER'] ?? '';
$allowed_domain = 'yourdomain.com';

if (!str_contains($referer, $allowed_domain)) {
    http_response_code(403);
    exit('Access denied');
}

Combined with CSRF tokens, this eliminates most direct access attempts.

5. Use PHPMailer or an API — Not mail()

PHP’s native mail() function gives you raw header construction, which is where injection lives. PHPMailer (kept updated — see CVE-2016-10033 above) or sending through an SMTP API treats headers as discrete fields rather than a raw string, making injection structurally much harder:

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->setFrom($sanitized_email, $sanitized_name);  // Discrete fields
$mail->addAddress('[email protected]');
$mail->Subject = $sanitized_subject;
$mail->Body    = $message_body;
$mail->send();

Even better: route outbound form emails through a transactional email API (Resend, SendGrid, Postmark) rather than your server’s mail infrastructure at all. This removes your server from the spam relay equation entirely.

The Validation Problem These Fixes Don’t Solve

All of the above protects your mail infrastructure. It doesn’t protect you from the upstream problem: bots and spammers submitting your form with valid-looking data that passes every technical check.

A bot can submit a real email address, a real name, a real subject line, and a spammy message — and your PHP sanitization will pass it through. The header injection is blocked, but you’re still writing spam to your database and sending yourself notifications for junk.

This is where content-level filtering matters. The validation layer (strip newlines, validate email format, CSRF tokens) and the spam detection layer (is this submission legitimate?) solve different problems.

InputGate handles the second layer — scoring the content of submissions for spam likelihood before your backend processes them. Used alongside proper PHP sanitization, you get protection at both levels:

  1. Your server doesn’t get abused as a spam relay
  2. Spam submissions that pass technical validation still get caught by content scoring

See the integration docs for how to wire up server-side validation alongside the InputGate API check.

A Note on Legacy PHP Forms

Many PHP contact forms in the wild were written in 2008 and never meaningfully updated. If you’ve inherited one of these, the path of least resistance is often replacement rather than remediation — spin up a clean form handler with current libraries, add CSRF protection, validate inputs properly, and integrate a spam check at the API level. The refactoring cost is lower than it looks, and the security posture improvement is significant.


Related reading: Why bots bypass traditional spam filters — understanding what you’re actually up against makes the defense decisions clearer.