Skip to main content

Comment faire planter un serveur de messagerie avec un seul e-mail

Écrit par

Joran Greef

crash an email server with single email small

1 août 2018

0 minutes de lecture

Cinq des analyseurs d’e-mails les plus populaires pour Node.js se sont récemment révélés vulnérables à une attaque par déni de service (DoS) triviale. La vulnérabilité peut être exploitée en intégrant quelques millions de pièces jointes vides dans un e-mail qui contournera les limites de taille habituelles (généralement 20 Mo ou moins). Lorsque cet e-mail est envoyé à un serveur de messagerie vulnérable, il bloque la boucle d’événements de Node.js pendant plusieurs secondes en raison du nombre considérable de pièces jointes. L’utilisation de la mémoire explose jusqu’à 2 Go ou plus en raison des objets internes créés pour chaque pièce jointe, ce qui suffit généralement à faire tomber tout le serveur à cause d’une erreur d’épuisement de la mémoire. Votre serveur Node.js analyse-t-il les e-mails ? Savez-vous quel analyseur d’e-mails vous utilisez ? Avant de vérifier, voyons qui est concerné.

Avant de continuer, voici l’inévitable XKCD.

E-mail manuscrit présentant des excuses à Kevin pour deux ans de retard, évoquant l’anxiété liée aux e-mails et déclinant une invitation LinkedIn, à côté d’une personne utilisant un ordinateur portable.

Une attaque par déni de service ne devrait pas être aussi facile, si ?

La vulnérabilité est facile à expliquer, facile à exploiter et touche des milliers de systèmes. La bibliothèque mailparser, par exemple, totalise jusqu’à 249 400 téléchargements mensuels et est utilisée comme dépendance par 214 autres projets, dont Sendgrid. Haraka est une autre bibliothèque concernée, utilisée par Craigslist, Fort Anti-Spam et ThreatWave.

La correction tient en une seule ligne. Pas besoin de Cloudflare : il suffit de valider les données utilisateur. Vous pouvez compter le nombre de pièces jointes (y compris les parties texte) et réagir si leur nombre dépasse, disons, 1 000. Quand avez-vous vu pour la dernière fois un e-mail avec 10 000 pièces jointes ? Quand avez-vous eu besoin d’en envoyer 100 000 ? Et si vous continuez à analyser les e-mails après un million de pièces jointes… eh bien, vous savez que vous êtes allé trop loin !

Attendez, comment avons-nous pu passer à côté ? Si la correction est aussi simple que nous le prétendons, comment au moins cinq implémentations ont-elles pu se tromper ? Et comment avons-nous découvert la vulnérabilité ? Que pouvons-nous faire pour progresser ?

Imaginez que vous écriviez un analyseur d’e-mails…

Vous savez combien de RFC vous devez lire (et interpréter). Vous savez combien de tests écrire pour vous assurer d’être aussi conforme que possible aux RFC. Vous connaissez le mantra logiciel : « d’abord, faire fonctionner, puis optimiser ». Mais les analyseurs d’e-mails sont difficiles à écrire, et vous serez déjà satisfait s’ils fonctionnent. Une fois que vous en avez écrit un, vous comprenez pourquoi vous ne faites pas forcément rapidement un calcul approximatif pour estimer la mémoire qu’un seul objet multipartie de votre conception pourrait allouer. Si vous effectuez une analyse de complexité :

Vous mesurerez probablement uniquement l’utilisation du processeur, pas celle de la mémoire. Vous ne mesurerez probablement pas l’empreinte mémoire habituelle. Vous finirez par ne pas tenir compte de l’environnement SMTP et ne chercherez donc pas à optimiser certains parcours dans votre analyseur, même si 90 % des e-mails analysés sont des spams. Vous éviterez d’imposer trop de règles strictes dans votre analyseur. Vos utilisateurs n’apprécieront peut-être pas une limite au nombre de pièces jointes par e-mail. Vous préférerez tout analyser et laisser l’utilisateur rejeter l’e-mail pendant la transaction SMTP, si nécessaire. Après tout, vous n’êtes pas l’administrateur du serveur de messagerie.

Imaginez que vous exploitiez un serveur de messagerie…

L’une des premières choses que vous ferez probablement sera de définir la taille limite des e-mails. Plus un e-mail est volumineux, moins les autres serveurs ont de chances de l’accepter. Vous fixez cette limite à seulement 20 Mo, en pensant que cela permettra aussi de maintenir le temps d’analyse dans des limites raisonnables. Vous choisissez un analyseur d’e-mails populaire et éprouvé. Vous partez du principe que sa complexité est linéaire, soit O(N), par rapport à la taille de l’e-mail. Vous mesurez l’utilisation du processeur avec une taille maximale de 20 Mo et vous vous attendez à ce que votre serveur traite des milliers de messages par seconde. Vous supposez que tous les e-mails de 20 Mo prendront à peu près le même temps à traiter. Vous estimez que 8 Go de RAM devraient suffire pour traiter 200 e-mails simultanés de 20 Mo par seconde, puisque vous ne pensez pas que votre base d’utilisateurs va croître si rapidement. Si quelqu’un vous mettait au défi de parier que votre serveur peut gérer 10 e-mails simultanés de 20 Mo, vous seriez confiant. La dernière chose à laquelle vous pensez, c’est une pièce jointe de 0 octet. Quel mal un fichier vide a-t-il jamais pu faire ?

Et puis, vous voyez ceci :

MIME-Version: 1.0
From: <trusting@user.data>
To: <validating@user.data>
Subject: MIME Multipart Attack
Date: Sat, 30 Jun 2018 15:51:58 +0000
Message-ID: <allocate_gigabytes_of_ram@node.js>
Content-Type: multipart/mixed; boundary="0"

--0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

--0
--0
--0
--0
--0
--0
--0
--0 [× 4 million]</allocate_gigabytes_of_ram@node.js></validating@user.data></trusting@user.data>

Comment avons-nous découvert la vulnérabilité ?

Ronomon est une jeune entreprise spécialisée dans les e-mails, actuellement en bêta privée. À mes débuts, j’ai eu la mauvaise surprise de voir des e-mails entrants bloquer la boucle d’événements de Node.js pendant des dizaines de secondes, à cause du ramasse-miettes de V8. J’ai passé deux jours de vacances à activer et désactiver des indicateurs de fonctionnalité toutes les quelques minutes pour réduire la charge, tout en essayant de comprendre ce qui se passait. Finalement, avec l’aide de Vyacheslav Egorov, j’ai commenté le fonctionnement interne de la fonction CollectAllAvailableGarbage de V8, qui effectuait allègrement sept cycles de collecte arbitraires sur un énorme tas de plusieurs gigaoctets. Avec le recul, cette expérience m’a beaucoup appris. Depuis, je me méfie énormément de l’allocation d’objets sur le tas et du fait de bloquer la boucle d’événements.

Au début de l’année dernière, j’ai entrepris d’écrire un nouvel analyseur d’e-mails, depuis publié en open source sous le nom de @ronomon/mime. L’objectif était de le rendre 10× plus rapide que le précédent, avec un nombre minimal d’allocations, en travaillant sur des tampons bruts et en respectant les RFC, avec une couverture de tests de 100 %, y compris des tests de fuzzing. L’objectif était difficile à atteindre et m’a amené à faire des choses comme éliminer les conversions de tampon en chaîne de caractères et réduire les erreurs de prédiction de branche causées par le Base64 découpé en lignes de 78 caractères.

En chemin, j’ai compris que certaines règles gagneraient à être appliquées dans l’analyseur d’e-mails plutôt que dans le serveur de messagerie, et inversement. Il s’agissait notamment de rejeter les encodages Base64, Quoted-Printable ou de caractères manifestement malveillants, corrompus ou tronqués, de rejeter les en-têtes critiques en double, de limiter le nombre de parties multiparties et de limiter les retours en arrière causés par de fausses détections de délimiteurs multiparties.

Au début de cette année, j’ai contacté Jamie Davis, auteur d’un excellent guide pour éviter de bloquer la boucle d’événements de Node.js. Jamie menait justement des recherches sur les attaques contre les boucles d’événements et je lui ai suggéré que certains analyseurs d’e-mails pourraient être vulnérables à une attaque multipartie. J’ai été surpris de constater que l’hypothèse se confirmait avec tous les analyseurs d’e-mails Node.js que j’ai testés.

Dix pistes d’amélioration

Voici dix conseils et bonnes pratiques à prendre en compte :

  1. Les attaques DoS exploitent la rareté des ressources. Faites preuve de sympathie mécanique et n’oubliez pas que vous écrivez pour une machine. Respectez les ressources matérielles qui vous sont confiées. Gérez-les de manière responsable : ne gaspillez rien. Ne vous contentez pas d’une complexité O(N) pour l’utilisation du processeur : visez aussi O(N) pour les allocations mémoire et chaque ressource que vous sollicitez. Si votre code utilise efficacement le processeur, la mémoire, le disque et le réseau, il sera moins vulnérable à l’épuisement des ressources et vous serez plus à même de définir des limites raisonnables pour chacune d’elles.

  2. Faites des calculs approximatifs pour toutes les dimensions des ressources dès le début de la conception. Vous repérerez ainsi les mauvaises conceptions plus tôt et éviterez de vous lancer dans des implémentations « impossibles ». Les performances et la sécurité ne s’optimisent pas et ne s’ajoutent pas après coup : elles doivent être intégrées dès le départ. N’attendez pas que votre module devienne populaire.

  3. Équilibrez l’utilisation des ressources dans toutes les dimensions. Vous avez peut-être assez de puissance processeur pour atteindre vos objectifs de débit, mais manquerez-vous de mémoire avant d’y parvenir ? Là encore, des calculs approximatifs vous aideront à équilibrer l’utilisation des ressources et à éviter les goulots d’étranglement dans votre conception.

  4. N’oubliez pas qu’un problème de performances n’attend que de devenir une attaque DoS, en particulier lorsque vous utilisez une boucle d’événements. La prochaine fois qu’un utilisateur vous signale un problème de performances, voyez-y une occasion de prévenir un problème de sécurité.

  5. Validez toutes les données utilisateur, pas seulement « combien », mais aussi « combien de fois ». En fait, ce sont souvent les petites choses qu’il faut surveiller, car elles sont fréquentes et peuvent être multipliées et amplifiées à vos dépens.

  6. Demandez-vous sans cesse ce qui vous semble réaliste. Ne laissez rien dépasser de 10× votre seuil de réalisme. Intégrez vos attentes au code que vous écrivez.

  7. Surveillez les zones de jonction entre les modules. Ne partez pas du principe que « quelqu’un d’autre s’en chargera ». Ne laissez pas les règles passer entre les mailles du filet. Il vous faudra peut-être mieux comprendre vos dépendances.

  8. Considérez les données manifestement incorrectes comme toxiques. N’y touchez pas, même avec un bâton de trois mètres. Éliminez-les dès que possible.

  9. Demandez-vous ce qu’un utilisateur malveillant pourrait faire. Ne vous contentez pas de relire votre code : essayez activement de l’exploiter. Réfléchissez à au moins trois façons de l’exploiter dans chaque module et corrigez-les avant de le publier. Fixez-vous cet objectif et trouvez-les. Elles existent toujours. Vous serez surpris.

  10. Les tests de fuzzing ont une imagination débordante. Écrivez vos propres tests de fuzzing simples pour générer un éventail aléatoire d’arguments valides et invalides. Pour vérifier l’exactitude des résultats, comparez les valeurs de retour de votre fonction à celles d’une autre implémentation pour les cas valides, et vérifiez les exceptions pour les cas invalides. Des tests de fuzzing qui exécutent des millions de permutations d’arguments de fonction, c’est la loi de Linus poussée à l’extrême : ils simulent la capacité de milliers de regards à repérer les bugs en quelques secondes.

Calendrier des divulgations privées et publiques

La vulnérabilité a été signalée en privé aux responsables des modules concernés le 23 avril 2018. Quelques jours avant l’échéance de divulgation publique de 90 jours, ils ont eu la possibilité de la reporter, pour quelque raison que ce soit. En outre, les responsables des modules dépendants les plus utilisés ont été contactés (lorsque leurs coordonnées étaient disponibles sur GitHub) et informés en vue de la divulgation publique prévue le 25 juin 2018.

Merci à Karen Yavine, Simon Maple et Danny Grander de Snyk pour leur aide concernant la divulgation publique, la suggestion de cet article et les investigations complémentaires. Merci également, en particulier, à Matt Sergeant de Haraka pour sa réactivité.

haraka

(versions < 2.8.19)

https://snyk.io/vuln/npm:haraka:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 15th, 2018 - Vulnerability fixed but not yet published to npm
June 25th, 2018 - Public disclosure
June 27th, 2018 - Version 2.8.19 published with fix

mailparser

(toutes les versions)

https://snyk.io/vuln/npm:mailparser:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for mailparser.

emailjs-mime-parser

(toutes les versions)

https://snyk.io/vuln/npm:emailjs-mime-parser:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for emailjs-mime-parser.

mailsplit

(versions < 4.2.1)

https://snyk.io/vuln/npm:mailsplit:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
July 23rd, 2018 - Version 4.2.1 published with fix

mailparser-mit

(toutes les versions)

https://snyk.io/vuln/npm:mailparser-mit:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for mailparser-mit.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.