1 points par GN⁺ 2024-05-22 | 2 commentaires | Partager sur WhatsApp
  • Dans un système qui générait des e-mails, des SMS et des PDF à partir de modèles de documents, un problème est apparu où seuls les points disparaissaient dans le corps des e-mails envoyés à certains clients
  • Le point était bien présent dans le modèle source, l’aperçu et le PDF, mais le bug ne se reproduisait qu’après remplacement des valeurs de placeholder par les données réelles d’un client, et uniquement à une certaine longueur de ligne
  • Un client SMTP personnalisé, en limitant la longueur des lignes, repoussait le point au début de la ligne suivante, et le serveur le supprimait ensuite à cause de la règle de dot transparency de SMTP
  • Le correctif a consisté à faire en sorte que le client ajoute un point supplémentaire devant toute ligne commençant par un point suivi d’autres caractères
  • Une autre équipe qui partageait le même code de client SMTP n’a pas appliqué le correctif, si bien que certains clients ont reçu des e-mails où leur prime mensuelle d’assurance semblait être de $2700 au lieu de $27.00 ; le bug a alors été corrigé immédiatement

Le problème commence dans un système central de modèles

  • Il y a environ 7 ans, un client voulait regrouper dans un système unique des documents dispersés dans plusieurs modèles Microsoft Word
  • Dans l’ancien fonctionnement, les employés remplaçaient eux-mêmes les placeholders par le prénom, le nom, etc., et il devenait difficile de gérer des modèles contenant d’anciennes conditions générales, d’anciens logos d’entreprise ou des polices incorrectes
  • Le nouveau système gérait les modèles de documents de façon centralisée et servait à générer, à partir d’eux, des documents PDF, des SMS et des corps d’e-mails
  • Même les messages de bienvenue pour les nouveaux clients pouvaient avoir des modèles différents selon le canal de diffusion
    • la version e-mail pouvait utiliser des tableaux HTML et un style basique
    • la version destinée au courrier postal pouvait inclure des infographies
    • la version SMS ne pouvait contenir qu’un court message de bienvenue

Un point qui ne disparaît que pour certains clients

  • Quelques mois, voire plus d’un an après la mise en service du système, un administrateur a signalé qu’un point manquait dans le corps d’un e-mail envoyé à un client précis
  • En envoyant le même e-mail à d’autres clients, le point ne disparaissait pas, ce qui rendait peu probable une simple erreur dans le modèle
  • Le point en question existait bien dans le code source du modèle
  • En copiant le modèle de production dans un environnement local pour générer un aperçu du corps de l’e-mail, le point apparaissait aussi, et il s’affichait normalement dans la version de sortie produite à partir du même modèle
  • En local, les e-mails étaient envoyés vers localhost sur un port spécifique, puis vérifiés via un faux serveur SMTP comme SMTP4dev et Outlook
  • Lors de la première vérification dans Outlook d’un e-mail envoyé depuis l’environnement local, le point s’affichait lui aussi normalement

Les données propres au client ont créé les conditions de reproduction

  • Lors de la génération d’un e-mail, d’un PDF ou d’un SMS, le modèle remplaçait les valeurs de placeholder comme le prénom et le nom du client par les données réelles
  • Même avec le même modèle, la longueur du corps des e-mails pouvait varier selon le client
  • En retrouvant les valeurs de placeholder réellement utilisées pour le client concerné puis en renvoyant l’e-mail en local avec ces mêmes valeurs, le phénomène du point disparu a pu être reproduit dans Outlook
  • Le problème dépendait donc de la longueur exacte du corps de l’e-mail et de son contenu pour ce client
  • Au départ, on a vérifié si le point dans le modèle était encodé d’une certaine façon ou s’il s’agissait d’un autre caractère ressemblant à un point, mais ce n’était pas la cause
  • En décalant le point d’un seul caractère dans le modèle, il réapparaissait dans Outlook, ce qui a montré que sa position dans la ligne était l’indice clé

Longueur des lignes SMTP et dot transparency

  • Le débogage a montré que le code qui enregistrait les e-mails en base de données ne modifiait pas le modèle autrement qu’en remplaçant les placeholders par les informations du client
  • L’enquête s’est ensuite concentrée sur le code d’une tâche cron qui récupérait périodiquement les destinataires avant d’envoyer les e-mails
  • Une partie du code appelée par cette tâche cron venait d’un projet précédent, notamment une implémentation de client SMTP personnalisé
  • Ce code contenait une fonction qui découpait les lignes du corps de l’e-mail pour qu’aucune ne dépasse un certain nombre de caractères
  • Ce comportement était lié à la limitation de longueur des lignes dans la spécification SMTP
    • la longueur totale maximale d’une ligne de texte est de 1000 octets en comptant <CRLF>
    • le point dupliqué en tête de ligne pour assurer la transparence n’est pas inclus dans ce calcul
    • cette limite peut être augmentée via les SMTP Service Extensions
  • Dans le corps d’e-mail problématique, la limite de longueur de ligne repoussait le point au premier caractère de la ligne suivante
  • Dans la spécification SMTP, la fin des données du message est indiquée par une ligne contenant uniquement un point
  • La règle de dot transparency de SMTP traite donc à part les lignes du corps qui commencent par un point
    • avant d’envoyer une ligne de texte, le client SMTP doit vérifier si son premier caractère est un point ; si oui, il doit insérer un point supplémentaire au tout début de la ligne
    • si le serveur SMTP reçoit une ligne qui ne contient qu’un point, il la traite comme la fin des données du message
    • si le premier caractère est un point et que d’autres caractères suivent sur la même ligne, le serveur supprime ce premier point
  • Le client SMTP personnalisé ne faisait pas ce doublage du point lorsqu’une ligne commençait par un point, et le serveur SMTP destinataire supprimait donc le premier point, ce qui effaçait le point réel du contenu

Le correctif et les effets révélés plus tard

  • Le correctif a consisté à faire en sorte que, lorsqu’une ligne commence par un point et contient d’autres caractères, le client ajoute un point supplémentaire au début de cette ligne
  • Ainsi, même si le serveur SMTP destinataire supprime le premier point, le point d’origine reste bien présent dans le contenu
  • En renvoyant localement l’e-mail original avec les mêmes informations de destinataire que pour le client concerné, le point ne disparaissait plus
  • Après le déploiement du correctif, les autres équipes ont été prévenues du bug, car le même code de client SMTP avait été repris depuis un projet précédent
  • Quelques mois plus tard, le système d’une autre équipe n’était toujours pas patché et a envoyé plusieurs e-mails importants informant des clients de leur nouvelle prime mensuelle d’assurance
  • Dans certains e-mails, le point décimal de la prime mensuelle tombait exactement au début d’une ligne et disparaissait
  • Résultat : certains clients ont reçu un e-mail laissant penser que leur nouvelle prime était de $2700 au lieu de $27.00
  • Ce bug dépendait de la longueur de chaque ligne du corps d’e-mail et ne touchait que certains clients dont la longueur du prénom et du nom créait exactement les bonnes conditions
  • Comme la cause était déjà connue, le code de cette équipe a été patché immédiatement

2 commentaires

 
surfindia 2024-05-22

On dirait qu’ils ont interprété « period » comme une durée et non comme un point dans le titre, haha.

 
GN⁺ 2024-05-22
Avis sur Hacker News
  • Une partie de ce code implémentait directement un client SMTP, et la cause profonde semble se trouver là
    Implémenter correctement un protocole est difficile, et les bugs comme celui décrit dans l’article sont fréquents
    Une bibliothèque de client SMTP correctement implémentée aurait encodé le texte d’entrée conformément aux règles SMTP, indépendamment de la position des points dans ce texte, et la couche de templates n’aurait pas eu à se soucier de SMTP

    • Le vrai problème tient moins au protocole lui-même qu’à l’approche cowboy adoptée pour le gérer
      Recevoir du texte et l’encoder conformément aux règles SMTP n’est pas difficile, mais il faut déjà savoir que ce traitement est nécessaire
      D’innombrables erreurs et failles de sécurité, comme l’injection SQL ou le XSS, viennent de la même erreur : concaténer des chaînes
      Dans une requête SQL, l’association des valeurs au template de requête doit se faire dans “l’espace SQL”, et non dans un espace de chaînes non typées ; fabriquer directement une chaîne SQL sérialisée comme SELECT * FROM foo WHERE foo.bar = + $userData est une mauvaise approche
      De même, si l’on manipule les templates HTML au niveau de l’arbre du document plutôt que comme une représentation textuelle, on évite des vulnérabilités idiotes
      Pour éviter que des points disparaissent dans un e-mail, il ne faut pas injecter du texte non structuré au milieu du pipeline SMTP ; il faut respecter le niveau d’abstraction auquel on travaille
      À ce sujet, langsec mérite aussi le détour
    • Le vrai problème est que SMTP est un protocole “en clair” avec de la signalisation in-band
      Cela arrive parce qu’une “ligne contenant uniquement un point” a été définie comme une séquence de contrôle, et non comme une ligne littérale
      SMTP est un exemple de conception inutilement complexe, et les bugs d’implémentation reflètent cette complexité
      Je ne recommande pas de l’implémenter soi-même, mais SMTP ne devrait pas être si difficile à implémenter correctement par une seule personne
    • Ça a l’air d’une bonne idée, mais si l’on ajoute une bibliothèque SMTP, celle-ci peut tirer 5 dépendances, chacune en tirant à son tour 3 transitives, dont l’une est en réalité un projet boîte à outils universelle dont on n’utilise que 1 %
      Le reste est du poids mort, mais à cause de fonctions qui ne sont même pas appelées, on se retrouve avec 20 dépendances de plus ; sans s’en rendre compte, la base de code gonfle de plusieurs Mo, et l’on reçoit des alertes CVE pour une bibliothèque dont on ignorait même l’usage
    • https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law
  • D’autres ont déjà mieux couvert les aspects techniques, mais cela me rappelle une anecdote sur l’importance que peut avoir une chose aussi minuscule qu’un simple point en fin de phrase
    En Allemagne, où je travaille, il est courant de demander, au moment de quitter un emploi, un “Zeugnis”, une lettre de référence décrivant les tâches effectuées et évaluant l’employé ; c’est un document important, généralement demandé lors d’une recherche d’emploi
    Évidemment, aucun employé n’accepterait une formulation du genre “cette personne est paresseuse, ne l’embauchez pas”, donc une sorte de code, le “Zeugnissprache”, s’est développé sous forme d’éloges déguisés
    L’un de ces codes voudrait que l’absence de point à la dernière phrase signifie : “ignorez tout ce qui est écrit ici, cette personne est nulle”
    Après mon dernier poste, j’ai fait relire mon Zeugnis par un avocat et, même si toutes les évaluations étaient positives, le point manquait à la dernière phrase, peut-être par négligence

    • L’idée qu’un code secret soit utilisé dans les lettres de recommandation est une légende urbaine
      Les RH n’ont aucun intérêt à créer un code secret à partager avec de potentiels concurrents, et l’idée de l’enseigner aux nouveaux responsables RH tout en le gardant secret n’a aucun sens
      Ce genre de légende naît parce que les RH ne peuvent pas écrire trop explicitement que quelqu’un a été un problème
      Quand il n’y a rien de positif à dire, elles louent des qualités banales comme la ponctualité ; c’est moins un code secret qu’une formule RH équivalente à “bless their heart”
      En cas de procès, cela laisse une porte de sortie : “Monsieur le juge, c’était un compliment ! Il arrivait toujours à l’heure !”
    • “Si la dernière phrase n’a pas de point, ignorez tout ce qui est écrit ici, cette personne est nulle” : difficile d’imaginer ce qui pourrait mal tourner
    • Je me demande s’ils ont aussi vérifié si des points manquaient dans les documents d’évaluation de performance
  • Je ne vois pas pourquoi une tâche cron qui envoie des e-mails devrait implémenter son propre client SMTP
    Il suffit d’utiliser un programme comme mail de mailutils
    Même du point de vue de la délivrabilité, bricoler soi-même une interaction SMTP minimale au-dessus d’un socket part déjà mal
    Aujourd’hui, on ne peut pas simplement se connecter à n’importe quel hôte d’échange mail pour envoyer un message ; il faut généralement se connecter à un relais SMTP spécifique fourni par son FAI
    Il faut alors implémenter une connexion TLS, l’authentification, etc.
    Le plus ironique, c’est que cron sait déjà envoyer du courrier
    La sortie d’une tâche cron est envoyée par e-mail à son propriétaire, et certaines implémentations de cron permettent de changer l’adresse de destination avec une variable comme MAILTO dans la crontab

    • Beaucoup de logiciels intègrent en réalité leur propre client SMTP, alors qu’ils devraient utiliser le MTA de l’hôte pour envoyer le courrier
      La raison est que l’utilisateur qui veut se servir de “ce truc” peut probablement saisir l’adresse d’un serveur SMTP, mais que le programme n’a aucun moyen de supposer qu’un MTA d’envoi comme sendmail est correctement configuré
      Dans les entreprises, il existe à grande échelle des serveurs incapables d’envoyer du courrier système, et cet état échappe souvent au contrôle de l’auteur du programme comme de l’utilisateur
      La configuration du courrier est un domaine spécialisé, avec des prérequis comme le DNS, le chiffrement et les politiques ; c’est beaucoup trop lourd si le seul objectif est qu’une application de portefeuille puisse envoyer un e-mail
      “Contactez votre administrateur système” n’est souvent pas une option réaliste, que ce soit à grande ou à petite échelle
      Ce n’est pas une bonne raison, mais c’est bien la raison réelle
    • Il n’existe pas vraiment de monde où l’on peut compter sur la présence d’un binaire système capable d’envoyer du courrier, mais je n’irais pas pour autant l’écrire moi-même
      Dans la plupart des langages, même si ce n’est pas dans la bibliothèque standard, il existe quantité de bibliothèques de client SMTP utilisables
    • Ça ressemble aussi à un cas de la loi de Zawinski, non ?
  • On voit ici deux très mauvaises habitudes
    La première, comme beaucoup l’ont souligné, est qu’il ne faut pas implémenter les standards à la va-vite
    Si vous devez le faire vous-même, il faut y consacrer l’attention et le soin nécessaires, ou utiliser une bibliothèque déjà existante
    La seconde est de ne pas vendorer ses dépendances
    Les bibliothèques utilisées doivent être mises à jour régulièrement et en temps voulu, pas seulement « quand c’est nécessaire »
    Si les mises à jour sont retardées, voire évitées, des bugs déjà corrigés en amont peuvent devenir de gros problèmes pour ceux qui pensaient ne devoir mettre à jour que lorsqu’ils voyaient directement le problème

    • L’alternative consistant à ne pas vendorer les dépendances me semble pire
      Dans ce cas, la stabilité de l’application est exposée aux changements amont, qui peuvent casser le code
      On ne reçoit peut-être pas les correctifs immédiatement, mais je préfère savoir que je change quelque chose parce que j’ai besoin d’un correctif précis, plutôt que d’introduire une instabilité indésirable et des risques supplémentaires
      Je suis plutôt dans le camp du « si ça n’est pas cassé, ne le réparez pas »
  • De nos jours, beaucoup de gens ne semblent plus apprendre les protocoles de base en interagissant directement avec eux dans un terminal
    SMTP semble avoir été conçu à l’origine pour ce type d’interaction manuelle, et pour l’avoir réellement utilisé ainsi pendant un temps, le « point seul sur une ligne » qui termine un message est gravé en permanence dans ma mémoire
    Dans le même ordre d’idées, l’échappement semble aussi être un concept étranger à beaucoup de programmeurs
    Face à la situation ci-dessus, beaucoup ne se demanderont pas « et si je veux envoyer un e-mail contenant une ligne avec seulement un point ? », mais un autre grand groupe trouvera cette question très logique et facile à comprendre

    • Je serais plutôt surpris qu’une proportion significative de développeurs logiciels ait appris de cette façon au cours des 30 dernières années
  • Il faut du dot stuffing
    SMTP https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
    Il existe aussi dans POP3 https://www.rfc-editor.org/rfc/rfc1939#page-8

  • Cela me rappelle une expérience de débogage d’une implémentation de protocole réseau. Plus précisément, pour les anciens, il s’agissait de AppleTalk NBP
    J’avais tout codé, mais mes paquets étaient rejetés, c’est-à-dire silencieusement droppés, tandis que ceux de l’implémentation Apple passaient
    J’ai affiché à l’écran un bon paquet et un mauvais paquet, je les ai comparés octet par octet, mais impossible de trouver le problème : ils étaient exactement identiques du début à la fin, checksum compris
    Arrivé à l’heure de partir, j’ai décidé d’imprimer ces trucs stupides pour les regarder plus tard, et dès l’impression, l’erreur m’a sauté aux yeux
    Ma version faisait deux pages, l’implémentation correcte une seule
    Je n’avais pas correctement vidé le buffer avant d’envoyer les données. C’est ça, les mbuf
    J’en ris encore quand j’y pense

  • La ligne « We are happy to welcome you to our family. » n’approche même pas de la limite de longueur de ligne
    Il devait se passer autre chose, par exemple le tout était peut-être en réalité une pièce jointe HTML MIME
    Il se peut qu’il y ait eu ... lots of text ... suivi de We are happy to welcome you to our family. de cette façon
    Mais si l’on coupe aveuglément du HTML en lignes, on casse les balises

    • Ce n’était qu’un exemple, désolé de ne pas pouvoir fournir un exemple réel avec le nombre exact de caractères
    • La plupart des personnes recevant un e-mail HTML cassé remarqueront simplement que le format de l’e-mail est bizarre, jugeront l’entreprise moins compétente parce qu’elle n’arrive même pas à écrire un e-mail lisible, puis passeront au suivant
      L’entreprise risque fort de ne jamais s’apercevoir de ce petit problème
    • En utilisant les retours à la ligne souples de l’encodage quoted-printable, on peut découper les lignes sans casser les balises HTML
      quoted-printable est censé avoir une limite maximale de 78 caractères, CRLF inclus, mais les clients e-mail sont généralement tolérants
  • Si vous n’avez jamais entendu parler du dot stuffing, vous aurez du mal à croire quelles autres horreurs existent dans le monde de l’e-mail
    Il y a le repliement des en-têtes, les guillemets dans la partie locale, les littéraux IPv6, etc.

    • Je me demande quel est le problème avec les littéraux IPv6
  • Dès que j’ai lu que c’était du code de client SMTP emprunté à un projet précédent, je m’attendais à une conclusion disant que l’autre équipe n’avait toujours pas patché ce bug