怎么读邮件头(顺便认出伪造的发件人)
邮件头是一封信如何抵达的唯一第一手记录。读懂 Authentication-Results、倒着走 Received 链,认出伪造发件人留下的那几处破绽。
有人转来一封邮件,问那个没法一句话回答的问题:「这是真的吗?」
正文帮不上忙。logo 几秒钟就能扒下来,语气可以模仿,而链接的显示文字从来就不必和它真正指向的地方一致。但每封邮件还带着另一份文件,一份发信方无法完全控制的文件——邮件头。那一段记录了哪些服务器经手过这封信、什么时候经手的,以及收信方对「这是谁发的」得出了什么结论。这是电子邮件里最接近保管链的东西。
这篇讲怎么读它。如果你想先补上那几个缩写背后的模型——SPF、DKIM、DMARC 各验什么、对齐为什么关键——SPF、DKIM、DMARC 到底各管什么讲的是那个。这里默认你已经有了,直接开始读一封真实的信。
先拿到原始邮件头
你要的是源码,不是客户端显示出来的样子:
- Gmail——打开邮件,右上角三点菜单,显示原始邮件。
- Outlook(桌面版)——文件 → 属性,读「Internet headers」那个框。
- Apple Mail——显示 → 邮件 → 所有标头,或者
⌥⌘U看原始源码。 - Thunderbird——
Ctrl+U。
从最上面一直复制到第一个空行为止。那个空行就是头部结束、正文开始的地方。
在把这段东西粘到任何地方之前,有个习惯值得先养成:邮件头里含有收件人地址、内网主机名,往往还有私有 IP 段。所以「交给哪个工具」是件需要挑的事。邮件头分析工具完全在你浏览器里解析,什么都不上传,而且可以在你复制出去的内容里把邮箱名遮掉,这样一段邮件头才好拿去和同事讨论。
从结论读起:Authentication-Results
别从文件最上面开始读。先看那一行——有服务器把自己的结论写在了那儿:
Authentication-Results: mx.example.net;
spf=pass smtp.mailfrom=bounces.provider.com;
dkim=pass header.d=yourcompany.com;
dmarc=pass header.from=yourcompany.com
把它读成三条独立结论,外加每条各自作用于哪个身份:
spf=pass smtp.mailfrom=...——连接过来的 IP 被授权替那个域名发信。注意看那个域名:它经常是服务商的退信域名,而不是 From 里的那个。dkim=pass header.d=...——有一个签名验过了,而且是那个域名的密钥签的。dmarc=pass header.from=...——SPF 或 DKIM 的通过,属于一个与可见 From 对齐的域名。三条里只有这一条说的是读者看到的那个发件人。
关于这个头有两点提醒,都很重要。
先看最上面的普通记录,但不能只因为它在最上面就相信它。 它通常是最新的结果,而链路上游任何人都可以插一行写着 dmarc=pass 的头。开头那个 authserv-id(上例里的 mx.example.net)指明是谁在下这个结论;只有当它属于一个你确实信任的邮件系统时才有意义——通常是你自己的服务商。来自陌生 authserv-id 的 dmarc=pass,仍然只是陌生人的说法。
它是记录,不是复核。 收信方在投递那一刻做了这些判断并把结果写下来。你现在做什么都不会让它们重跑一遍,而 DNS 可能早就变了。
如果这个头压根不存在,那也证明不了什么——很多系统就是不加。它只说明这条捷径没有,你得把剩下的读得更仔细些。
当 spf=pass 和 dmarc=fail 并排出现
这个组合最容易让人以为撞见了 bug,也是最值得一眼认出来的东西:
Authentication-Results: mx.example.net;
spf=pass smtp.mailfrom=bounces.mailer.example;
dkim=none;
dmarc=fail header.from=yourcompany.com
什么都没坏。SPF 确实通过了——是替 bounces.mailer.example 通过的,而这个域名跟 yourcompany.com 没有任何关系。又没有 DKIM 签名来补上另外半边。于是没有任何一个经过认证的身份与 From 头对齐,DMARC 判失败。
这个形态有两种截然不同的解释,把它们分开才是真正要做的事:
- 一个从没配过对齐的合法发信方——某个营销平台或工单系统以你的名义发信,却没有在你的域名下做 DKIM 签名。极其常见,在服务商那边就能修。
- 彻头彻尾的伪造——别人的域名通过了 SPF,而 From 里放的是你的名字。
剩下的邮件头,就是用来区分这两者的。
倒着走 Received 链
Received: 是路由历史。关键机制是:每台服务器都把自己那行加在最前面。所以这条链是最新的在上——最顶上那条是最后一跳,最下面那条自称是起点。
Received: from mx.example.net by inbox.example.net; Tue, 4 Aug 2026 10:00:12 +0000
Received: from mail.sender.example (mail.sender.example [198.51.100.7])
by mx.example.net; Tue, 4 Aug 2026 10:00:09 +0000
从下往上读,就是顺着时间跟着这封信走。有三样东西值得提取出来:
它从哪儿进入你的基础设施。 找到最下面那条由你信任的服务器写的记录,那就是边界——这条线很重要,因为它下面的每一行都是别人写的。一封伪造的邮件可以在那条线以下携带一整段编造的历史,主机名和时间戳都编得很像。边界以下,把每一跳都当成「说法」而不是「事实」。
方括号里的 IP。 取方括号里的字面地址([198.51.100.7]),而不是它前面那个主机名:主机名是连接方自称的,方括号里的地址才是你的服务器实际观察到的。如果来源 IP 和发信方的身份对不上——某银行的邮件从一个不相干国家的住宅宽带段发出来——这就值得追下去。丢进 IP 地址查询看看它到底归谁。
跳与跳之间的时间。 每一跳都带时间戳。差几分钟通常只是排队。更值得注意的是对不上的间隔——一封信在某处躺了几个小时,或者各跳时间戳是倒着走的(这多半意味着某台机器时钟不准,偶尔意味着这条链是手工拼出来的)。
身份字段,以及它们互相打架的地方
一封邮件会在好几个地方声明「我是谁发的」,而伪造往往就表现为这些地方互相矛盾。
From:——读者看到的那个。 注意显示名是自由文本,跟真实地址没有任何关系:
From: "Support <support@yourbank.example>" <ceo@random-domain.example>
多数客户端只显示 Support <support@yourbank.example>,而真正的地址是后面那个。显示名里嵌着一个邮箱地址,可能是刻意误导。 操作前要核对真正的 From 地址;把它放回上下文看,它是很强的警示,但单独一项还不构成证据。
Return-Path:——退信去哪儿。 这是投递时记录的信封发件人。它和 From 域名不一致是家常便饭且完全合法——那通常只是服务商在替你处理退信。它可以提供背景,但不能证明 SPF 实际评估的是谁:SPF 可能评估 SMTP MAIL FROM,信封发件人为空时也可能评估 HELO。有的话,请看 Authentication-Results 里的 smtp.mailfrom 或 smtp.helo,那才是接收方记录下来的身份。
Reply-To:——你的回复去哪儿。 在不少配置里同样合法(共享收件箱、客服系统)。但在一封本来就 DMARC 失败的邮件里,一个指向不相干域名的 Reply-To,就是把整段对话导向攻击者的那个机关。
Message-ID:——分配的标识符。 它的域名部分通常是生成它的系统。服务商代发时与 From 域名不同属于正常。格式错乱或者干脆没有,值得记一笔。
单看任何一项都定不了罪。这正是重点:信号在于模式,而不在于某一个字段。一个 DMARC 失败的 From 域名,加上一个藏着别的地址的显示名,再加上一个指向别处的 Reply-To——这三样凑成一个自洽的故事,而单纯的配置错误讲不出这个故事。
读一个 DKIM 签名
如果有 DKIM-Signature: 头,几个标签决定了它值多少:
DKIM-Signature: v=1; a=rsa-sha256; d=yourcompany.com; s=selector1;
h=from:to:subject:date; bh=...; b=...
d=——签名域名。拿它和 From 域名比。一个来自不相干域名的、完全合法的签名,对 DMARC 毫无贡献。s=——selector,它和d=一起定位到selector1._domainkey.yourcompany.com上的公钥。你可以用 DNS 查询工具自己把那条 TXT 取出来,确认那儿确实发布了密钥。h=——被签名覆盖的头部列表。一个不含from的签名基本等于没有:可见发件人可以在不惊动它的情况下被换掉。这一项值得专门看,因为它是一种很隐蔽的失效——出问题了它照样报dkim=pass。l=——正文长度上限,只签前 N 个字节。超出部分追加的内容不在签名内。不常见,一旦出现就值得警惕。
有一条限制必须说清楚:读签名不等于验签。验签需要未经改动的正文和 DNS 里的公钥,而且传输途中任何改动都会让一条本来完好的签名失效。分析工具读的是这些字段、检查上面那些结构性质;Authentication-Results 里的 dkim=pass 才是收信方在投递时做的验签。
ARC 那几个头,以及它们值多少
转发和邮件列表天生就会打断认证:中继用的是它自己的 IP,SPF 于是失败;列表动过邮件,DKIM 也跟着失效。ARC(认证接收链,RFC 8617)存在的意义,就是把断裂之前的那个判决带过来。读 Google 或微软经手的真实邮件,你会不停看到它:
ARC-Seal: i=1; a=rsa-sha256; cv=none; d=lists.example.org; s=arc; b=...
ARC-Message-Signature: i=1; a=rsa-sha256; d=lists.example.org; s=arc;
h=from:to:subject; b=...
ARC-Authentication-Results: i=1; lists.example.org;
spf=pass smtp.mailfrom=you@yourcompany.com;
dkim=pass header.d=yourcompany.com; dmarc=pass
每经过一个中间方就是三个头,用 i= 编号:
ARC-Authentication-Results——那一跳当时看到的认证结果,是它动手改动之前的样子。这是真正有内容的那个。ARC-Message-Signature——那一跳对邮件本身的签名,让它留下的这份快照事后改不了。ARC-Seal——对链本身的签名,正是它让这些记录成为一条链,而不是一堆各说各话的声明。
i= 读作位置:i=1 是第一个中间方,i=2 是在它之后重新封签的。在编号最大的那条 ARC-Seal 上,cv= 报告的是链校验结果——第一跳是 none,此前的链验过了是 pass,没验过是 fail。看到 cv=fail 就意味着链在某处断了,它携带的那些结果什么也证明不了。
接下来是判断一封信时真正要紧的那点:ARC 不是绕过机制,链有效也不等于 DMARC 通过。 任何人都能挂一组 ARC 上去,声称这封信什么都通过了——攻击者也能。它只有在最终收信方信任那个封签域名时才算证据,而这跟你前面对 authserv-id 用的是同一套判断。收信方自己决定认哪些中间方;ARC 给的是做这个决定的材料,不是必须认的义务。
所以把 ARC 链当解释读,别当判决读:它告诉你一封转发过来的信为什么 SPF 和 DKIM 都没过却照样投递到了。分析工具会报出找到几组 ARC 以及链校验状态,让你一眼看出这封信到底有没有经过中间方。
「伪造」和「合法但配错」并排看
两者都 DMARC 失败。区分靠的是邮件头:
| 信号 | 配错了但合法 | 大概率是伪造 |
|---|---|---|
| 来源 IP | 某个已知服务商的地址段 | 不相干的主机商、住宅宽带,或国别对不上 |
| SPF 域名 | 服务商的退信域名 | 一个不相干的一次性域名 |
| DKIM | 没有,或 d= 是服务商的 | 没有,或由不相干域名签的 |
| 显示名 | 平平无奇 | 里面嵌着邮箱地址,或在模仿另一个发件人 |
| Reply-To | 没有,或同一组织 | 不相干的域名 |
| Received 链 | 连贯,主机名合理 | 有断裂、时间戳异常、边界以下有编造的跳 |
| 整体模式 | 只有一处不对 | 好几处指向同一个方向 |
诚实的总结是:孤立的一处异常通常意味着配置有误;好几处指向同一个方向,就该把这封信当成敌意的来处理。
一份阅读清单
- 拿到原始源码,复制到第一个空行为止。
- 先读
Authentication-Results——并确认 authserv-id 属于你信任的系统。 - 记下每条结论作用于哪个域名,而不只是通过还是失败。陌生域名上的
spf=pass对 From 那一行什么都证明不了。 - 在 Received 链里找到你的信任边界,它以下的都是说法。
- 取入口那一跳方括号里的 IP,查清它归谁。
- 比对身份字段——From 地址与显示名、Return-Path、Reply-To、Message-ID。
- 检查 DKIM 签名的
d=和h=,确认from被覆盖。 - 如果有 ARC,看编号最大那条的
cv=——并且问一句:封签的那个域名你信得过吗。 - 权衡整体模式,而不是某一个字段。
与其每次都靠肉眼这么过一遍,不如把这段头粘进邮件头分析工具:它会把逐跳路径连同每跳耗时铺开,把认证结果真正解析出来而不只是原样回显,标出显示名欺骗和身份错配,并把 DKIM 签名逐个字段拆开。全程本地运行,邮件不离开你的机器。
而如果结论是你自己的域名正在被伪造,或者你某个合法发信方过不了对齐,那就不是取证问题而是配置问题了。用 SPF/DMARC 检测工具看看你的域名公布了什么;如果答案是强制还没开,从 p=none 走到 p=reject 讲的就是怎么分阶段把它开起来而不在路上弄丢邮件。