PHP 安全 E-mail

How email header injection works, how to stop it, and pre-send validation.

Intermediate
Key PointsNewlines inject extra headers — strip `\r\n` from anything entering headers.

Header injection

Headers are line-based plain text. A newline in user input creates a new field — a Bcc: turns your server into a spam relay.

PHP
<?php
// 攻击者提交的邮箱
$email = "victim@example.com\r\nBcc: a@evil.com, b@evil.com";

// 直接拼进头部
$headers = "From: " . $email;
echo str_replace("\r\n", "\\r\\n", $headers), "\n";
echo "字段数: " . (substr_count($headers, "\r\n") + 1), "\n";
Output
From: victim@example.com\r\nBcc: a@evil.com, b@evil.com
字段数: 2

One From became From plus Bcc. A single submission turns the server into a spam sender.

Defence

PHP
<?php
function buildMail(string $to, string $subject): array
{
    if (filter_var($to, FILTER_VALIDATE_EMAIL) === false) {
        throw new InvalidArgumentException("非法收件人");
    }

    if (preg_match("/[\r\n]/", $to . $subject)) {
        throw new InvalidArgumentException("检测到头部注入");
    }

    if (mb_strlen($subject) > 100) {
        throw new InvalidArgumentException("主题过长");
    }

    return ["to" => $to, "subject" => $subject];
}

$cases = [
    ["tom@example.com", "你好"],
    ["tom@example.com\r\nBcc: evil@x.com", "你好"],
    ["tom@example.com", "促销\r\nContent-Type: text/html"],
];

foreach ($cases as [$to, $sub]) {
    try {
        $m = buildMail($to, $sub);
        echo "已构建: " . $m["to"] . " | " . $m["subject"] . "\n";
    } catch (InvalidArgumentException $e) {
        echo "拒绝: " . $e->getMessage() . "\n";
    }
}
Output
已构建: tom@example.com | 你好
拒绝: 检测到头部注入
拒绝: 检测到头部注入
FILTER_VALIDATE_EMAIL already rejects newlines, so the first gate catches most. But subjects, display names and Reply-To need their own checks — no filter_var applies.

A complete safe sender

PHP
<?php
declare(strict_types=1);

final class SafeMailer
{
    public function __construct(private string $from, private string $fromName) {}

    public function send(string $to, string $subject, string $body): bool
    {
        $this->assertNoInjection($to, $subject, $this->fromName);

        if (filter_var($to, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException("收件人格式错误");
        }

        $headers = [
            "MIME-Version: 1.0",
            "Content-Type: text/plain; charset=UTF-8",
            "From: " . $this->encodeName($this->fromName) . " <{$this->from}>",
        ];

        return mail($to, $this->encodeName($subject), $body, implode("\r\n", $headers));
    }

    private function assertNoInjection(string ...$parts): void
    {
        foreach ($parts as $v) {
            if (preg_match("/[\r\n]/", $v)) {
                throw new InvalidArgumentException("检测到头部注入");
            }
        }
    }

    private function encodeName(string $v): string
    {
        return preg_match('/[^\x20-\x7e]/', $v)
            ? "=?UTF-8?B?" . base64_encode($v) . "?="
            : $v;
    }
}

$m = new SafeMailer("noreply@example.com", "我的站点");
try {
    $m->send("tom@example.com", "账户激活", "请点击链接激活");
    echo "已发送\n";
} catch (Throwable $e) {
    echo "失败: " . $e->getMessage() . "\n";
}
Output
已发送

Other mail risks

RiskMitigation
被当成开放中继收件人域名白名单、限流
表单被刷爆验证码、IP 限流、蜜罐字段
钓鱼内容正文不允许用户完全控制
附件带毒服务端扫描,或干脆不支持附件
日志泄露邮箱日志里脱敏 t***@example.com
An unthrottled contact form becomes a spam gateway within hours and your IP gets blacklisted. Rate limiting is a launch requirement.

Validation order

  1. Length cap —— stop huge inputs before regex runs
  2. Newline check —— stop header injection
  3. Format check —— filter_var or regex
  4. Business rules —— blocklist, unsubscribed
  5. Throttle —— per IP and per address

Friends & Partners