L’étrange affaire du point disparu
(tjaart.substack.com)- 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
localhostsur 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
- la longueur totale maximale d’une ligne de texte est de 1000 octets en comptant
- 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
$2700au 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
On dirait qu’ils ont interprété « period » comme une durée et non comme un point dans le titre, haha.
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
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 =+$userDataest une mauvaise approcheDe 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
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
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
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
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 !”
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
mailde mailutilsMê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
MAILTOdans la crontabLa 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
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
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
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
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 deWe are happy to welcome you to our family.de cette façonMais si l’on coupe aveuglément du HTML en lignes, on casse les balises
L’entreprise risque fort de ne jamais s’apercevoir de ce petit problème
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.
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