1 Punkte von GN⁺ 2024-05-22 | 2 Kommentare | Auf WhatsApp teilen
  • In einem System, das aus Dokumentvorlagen E-Mails, SMS und PDFs erzeugte, trat ein Problem auf, bei dem nur im Text einer an einen bestimmten Kunden gesendeten E-Mail ein Punkt verschwand
  • In der ursprünglichen Vorlage, der Vorschau und dem PDF war der Punkt vorhanden, doch nach dem Ersetzen kundenspezifischer Placeholder-Werte durch echte Daten ließ sich das Problem nur bei einer bestimmten Zeilenlänge reproduzieren
  • Ein eigener SMTP-Client begrenzte die Zeilenlänge und schob den Punkt an den Anfang einer neuen Zeile; aufgrund der SMTP-Regel zur dot transparency entfernte der Server diesen Punkt
  • Die Korrektur bestand darin, dass der Client vor Zeilen, die mit einem Punkt beginnen und danach weitere Zeichen enthalten, einen zusätzlichen Punkt voranstellt
  • Ein anderes Team, das denselben SMTP-Client-Code mitnutzte, hatte den Patch nicht eingespielt; dadurch wurden an einige Kunden E-Mails verschickt, in denen die monatliche Versicherungsprämie als $2700 statt $27.00 erschien. Der Bug wurde sofort behoben

Das Problem begann im zentralen Vorlagensystem

  • Vor etwa sieben Jahren wollte ein Kunde Dokumente, die über mehrere Microsoft-Word-Vorlagen verteilt waren, in ein einziges System konsolidieren
  • Beim bisherigen Vorgehen ersetzten Mitarbeitende die Placeholder in Dokumenten manuell durch Vorname, Nachname usw.; zugleich kursierten Vorlagen mit alten Geschäftsbedingungen, früheren Firmenlogos und falschen Schriftarten, was die Verwaltung erschwerte
  • Das neue System verwaltete Dokumentvorlagen zentral und erzeugte daraus PDF-Dokumente, SMS-Nachrichten und E-Mail-Texte
  • Auch Begrüßungsnachrichten für neue Kunden konnten je nach Zustellweg unterschiedliche Vorlagen haben
    • Die E-Mail-Version konnte HTML-Tabellen und einfache Styles verwenden
    • Die Version für den Postversand konnte Infografiken enthalten
    • Die SMS-Version konnte nur einen kurzen Begrüßungstext enthalten

Ein Punkt, der nur bei einem bestimmten Kunden verschwand

  • Einige Monate oder mehr als ein Jahr nach Inbetriebnahme des Systems meldete ein Administrator, dass im Text einer an einen bestimmten Kunden gesendeten E-Mail ein Punkt fehlte
  • Wurde dieselbe E-Mail an andere Kunden gesendet, verschwand der Punkt nicht, weshalb es kaum ein einfacher Vorlagenfehler sein konnte
  • Im Quellcode der Vorlage war der betreffende Punkt tatsächlich vorhanden
  • Auch als die Produktionsvorlage in die lokale Umgebung kopiert und der E-Mail-Text als Vorschau erzeugt wurde, war der Punkt sichtbar; ebenso wurde er in der mit derselben Vorlage erzeugten Druckversion korrekt angezeigt
  • Die lokale Umgebung funktionierte so, dass E-Mails an localhost auf einem bestimmten Port gesendet und mit einem Fake-SMTP-Server wie SMTP4dev sowie Outlook geprüft wurden
  • Auch als die erste lokal gesendete E-Mail in Outlook überprüft wurde, wurde der Punkt korrekt angezeigt

Kundenspezifische Daten als Reproduktionsbedingung

  • Beim Erzeugen von E-Mails, PDFs und SMS ersetzte die Vorlage Placeholder-Werte wie Vor- und Nachname des Kunden durch echte Daten
  • Selbst bei derselben Vorlage konnte sich die Länge des E-Mail-Texts je Kunde unterscheiden
  • Als die tatsächlich für den betroffenen Kunden verwendeten Placeholder-Werte ermittelt und lokal mit denselben Werten erneut eine E-Mail gesendet wurde, ließ sich in Outlook beobachten, dass der Punkt verschwand
  • Das Problem hing von der konkreten Textlänge und dem Inhalt dieser Kunden-E-Mail ab
  • Zunächst wurde geprüft, ob das Punktzeichen in der Vorlage codiert war oder ob es sich nicht um einen echten Punkt, sondern um ein anderes Zeichen handelte; das war jedoch nicht die Ursache
  • Als die Position des Punkts in der Vorlage um ein Zeichen verschoben wurde, war der Punkt in Outlook wieder sichtbar; die Zeilenposition wurde damit zum entscheidenden Hinweis

SMTP-Zeilenlänge und dot transparency

  • Das Debugging ergab, dass der Code, der die E-Mail in der Datenbank speicherte, die Vorlage außer dem Ersetzen der Placeholder durch Kundeninformationen nicht veränderte
  • Danach verengte sich die Untersuchung auf den Code des Cron-Jobs, der regelmäßig die Empfänger abruft und E-Mails versendet
  • Ein Teil des vom Cron-Job aufgerufenen Codes stammte aus einem früheren Projekt und enthielt eine eigene SMTP-Client-Implementierung
  • In diesem Code gab es eine Funktion, die Zeilen im E-Mail-Text umbrach, damit keine Zeile länger als eine bestimmte Zeichenzahl wurde
  • Dieses Verhalten hängt mit der Zeilenlängenbegrenzung der SMTP-Spezifikation zusammen
    • Die maximale Gesamtlänge einer Textzeile beträgt einschließlich <CRLF> 1000 octets
    • Ein führender Punkt, der aus Transparenzgründen dupliziert wird, wird dabei nicht mitgezählt
    • Durch SMTP Service Extensions kann dieser Wert erhöht werden
  • Im betroffenen E-Mail-Text wurde der Punkt wegen der Zeilenlängenbegrenzung an den Anfang der nächsten Zeile verschoben
  • In der SMTP-Spezifikation wird das Ende der mail data durch eine Zeile behandelt, die nur aus einem einzelnen Punkt besteht
  • Die SMTP-Regel zur dot transparency behandelt Textzeilen, die mit einem Punkt beginnen, gesondert
    • Bevor der SMTP-Client eine Mail-Textzeile sendet, muss er prüfen, ob das erste Zeichen ein Punkt ist; falls ja, muss er am Zeilenanfang einen weiteren Punkt einfügen
    • Der SMTP-Server behandelt eine Zeile, die nur aus einem Punkt besteht, als Ende der Mail-Daten
    • Wenn das erste Zeichen ein Punkt ist und in derselben Zeile weitere Zeichen folgen, löscht der Server das erste Zeichen
  • Der eigene SMTP-Client führte beim Beginn einer Zeile mit einem Punkt keine zusätzliche Punkt-Einfügung durch, und der empfangende SMTP-Server löschte den ersten Punkt, wodurch der tatsächliche Punkt im Text verschwand

Korrektur und später sichtbar gewordene Folgen

  • Die Korrektur bestand darin, dass der Client einen zusätzlichen Punkt voranstellt, wenn eine Zeile mit einem Punkt beginnt und in derselben Zeile weitere Zeichen folgen
  • Wird die E-Mail so gesendet, bleibt der ursprüngliche Punkt im Text erhalten, auch wenn der empfangende SMTP-Server den ersten Punkt entfernt
  • Als die ursprüngliche E-Mail lokal erneut mit denselben Empfängerinformationen des betroffenen Kunden gesendet wurde, verschwand der Punkt nicht mehr
  • Nachdem die Korrektur ausgerollt war, wurde auch ein anderes Team über den Bug informiert, weil es denselben SMTP-Client-Code aus dem früheren Projekt übernommen hatte
  • Einige Monate später war das System des anderen Teams noch nicht gepatcht und verschickte mehrere wichtige E-Mails, in denen Kunden über ihre neue monatliche Versicherungsprämie informiert wurden
  • In einigen E-Mails landete der Dezimalpunkt der monatlichen Versicherungsprämie exakt am Anfang einer Zeile und verschwand
  • Infolgedessen erhielten einige Kunden E-Mails, in denen die neue Prämie wie $2700 statt $27.00 aussah
  • Dieser Bug hing von der Länge jeder Zeile im E-Mail-Text ab und trat nur bei einigen Kunden auf, deren Vor- und Nachname genau die passende Länge hatten
  • Da die Ursache bereits bekannt war, wurde der Code dieses Teams sofort gepatcht

2 Kommentare

 
surfindia 2024-05-22

Ich glaube, im Titel wurde period nicht als Punkt, sondern als Zeitraum verstanden, haha.

 
GN⁺ 2024-05-22
Hacker-News-Kommentare
  • Dieser Code implementierte offenbar teilweise einen SMTP-Client selbst, und die eigentliche Ursache scheint dort zu liegen.
    Ein Protokoll korrekt zu implementieren ist schwierig, und Bugs wie der im Artikel beschriebene kommen häufig vor.
    Eine ordentlich implementierte SMTP-Client-Bibliothek hätte den Text unabhängig von der Position eines Punkts im Eingabetext gemäß der SMTP-Spezifikation kodiert, und die Template-Schicht hätte sich nicht um SMTP kümmern müssen.

    • Das eigentliche Problem liegt weniger im Protokoll selbst als im Cowboy-Ansatz beim Umgang damit.
      Text gemäß der SMTP-Spezifikation entgegenzunehmen und zu kodieren ist nicht schwer, aber man muss überhaupt wissen, dass diese Verarbeitung nötig ist.
      Zahlreiche Fehler und Sicherheitslücken wie SQL-Injection und XSS entstehen aus demselben Fehler, nämlich dem Aneinanderhängen von Strings.
      Bei SQL-Queries muss das Binden von Werten an Query-Templates im „SQL-Raum“ stattfinden, nicht in einem typlosen String-Raum; serialisierte SQL-Strings direkt zu bauen wie SELECT * FROM foo WHERE foo.bar = + $userData ist der falsche Weg.
      Ebenso lassen sich dumme Schwachstellen in HTML-Templates vermeiden, wenn man sie auf Ebene des Dokumentbaums behandelt statt als String-Darstellung.
      Um zu verhindern, dass Punkte in E-Mails verschwinden, sollte man keinen unstrukturierten Text mitten in eine SMTP-Pipeline injizieren, sondern die Abstraktionsebene respektieren, auf der man arbeitet.
      In dem Zusammenhang ist auch langsec einen Blick wert.
    • Das eigentliche Problem ist, dass SMTP ein „Klartext“-Protokoll mit In-Band-Signalisierung ist.
      So etwas passiert, weil „eine Zeile, die nur aus einem Punkt besteht“ nicht als wörtliche Zeile, sondern als Steuersequenz definiert wurde.
      SMTP ist ein Beispiel für unnötig komplexes Design, und Implementierungsbugs spiegeln diese Komplexität wider.
      Ich würde niemandem empfehlen, es selbst zu implementieren, aber SMTP sollte nicht so schwierig sein, dass man es allein kaum korrekt umsetzen kann.
    • Klingt nach einer guten Idee, aber wenn man eine SMTP-Bibliothek einbindet, zieht diese vielleicht fünf Abhängigkeiten mit, die jeweils drei transitive Abhängigkeiten nachziehen, und eine davon ist womöglich ein Allzweck-Werkzeugkasten-Projekt, von dem man tatsächlich nur 1 % nutzt.
      Der Rest ist totes Gewicht, zieht aber wegen Funktionen, die nie aufgerufen werden, 20 weitere Abhängigkeiten nach, und plötzlich bläht sich die Codebasis um mehrere MB auf, während man CVE-Warnungen für eine Bibliothek bekommt, von der man nicht einmal wusste, dass man sie verwendet.
    • https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law
  • Die technischen Aspekte haben andere schon besser behandelt, aber mir fällt eine Anekdote dazu ein, wie wichtig so eine Kleinigkeit wie ein einzelner Punkt am Satzende sein kann.
    In Deutschland, wo ich arbeite, ist es üblich, beim Ausscheiden ein „Zeugnis“ anzufordern, also ein Empfehlungsschreiben mit Tätigkeitsbeschreibung und Bewertung des Mitarbeiters; bei Bewerbungen ist es meist ein wichtiges Dokument.
    Natürlich würde kein Arbeitnehmer eine Formulierung wie „Diese Person ist faul, stellen Sie sie nicht ein“ akzeptieren, also ist mit der „Zeugnissprache“ eine Art Code entstanden, der wie Lob klingt.
    Einer dieser Codes soll sein, dass ein fehlender Punkt im letzten Satz bedeutet: „Ignorieren Sie alles, was hier steht; diese Person ist miserabel.“
    Nach meinem letzten Job ließ ich mein Zeugnis von einem Anwalt prüfen, und obwohl alle Bewertungen positiv waren, fehlte im letzten Satz der Punkt — ob aus Nachlässigkeit oder nicht.

    • Dass in Empfehlungsschreiben Geheimcodes verwendet werden, ist eine urbane Legende.
      Personaler haben keinen Anreiz, mit potenziellen Konkurrenten einen Geheimcode zu entwickeln, und es ergibt auch keinen Sinn, ihn neuen Personalern beizubringen und zugleich geheim zu halten.
      Solche Legenden entstehen, weil Personaler nicht allzu offen schreiben können, dass jemand ein Problemfall war.
      Wenn es nichts Positives zu sagen gibt, lobt man eben banale Stärken wie Pünktlichkeit; das ist weniger ein Geheimcode als das „bless their heart“ der HR-Sprache.
      Wenn jemand klagt, lässt es einem die Ausrede: „Euer Ehren, das war positiv gemeint! Er kam immer pünktlich!“
    • „Wenn im letzten Satz der Punkt fehlt, ignorieren Sie alles, was hier steht; diese Person ist miserabel“ — was könnte da schon schiefgehen?
    • Ich frage mich, ob sie geprüft haben, ob auch im Leistungsbeurteilungsdokument der Punkt fehlte.
  • Ich verstehe nicht, warum ein Cronjob, der E-Mails verschickt, einen eigenen SMTP-Client implementieren muss.
    Man kann einfach ein Programm wie mail aus mailutils verwenden.
    Auch aus Sicht der Zustellbarkeit ist es von Anfang an fragwürdig, eine minimale SMTP-Interaktion direkt über einem Socket selbst zusammenzubauen.
    Heutzutage kann man E-Mails nicht einfach durch direkte Verbindung zu irgendeinem Mail-Exchange-Host versenden; normalerweise muss man sich mit einem bestimmten SMTP-Relay-Host verbinden, den der ISP bereitstellt.
    Dafür muss man TLS-Verbindungen, Authentifizierung usw. implementieren.
    Leicht ironisch ist, dass cron selbst bereits E-Mails verschicken kann.
    Die Ausgabe eines Cronjobs wird dem Besitzer per Mail zugeschickt, und bei manchen cron-Implementierungen lässt sich die Empfängeradresse etwa über die Variable MAILTO in der crontab ändern.

    • Es gibt tatsächlich viele Dinge, die einen eigenen SMTP-Client einbauen, obwohl sie zum Versenden von Mail den MTA des Hosts verwenden sollten.
      Der Grund ist, dass ein Nutzer, der „dieses Ding“ verwenden will, zwar vermutlich eine SMTP-Server-Adresse eingeben kann, das Programm aber nicht darauf vertrauen kann, dass ein sendender MTA wie sendmail korrekt eingerichtet ist.
      In Unternehmen gibt es massenhaft Server, die keine Systemmails verschicken können, und oft liegt dieser Zustand außerhalb der Kontrolle des Programmautors oder des Nutzers.
      Mail-Konfiguration ist ein Spezialgebiet mit Voraussetzungen wie DNS, Verschlüsselung und Richtlinien; nur damit etwa eine Wallet-App E-Mails verschicken kann, ist das überdimensioniert.
      „Wenden Sie sich an Ihren Systemadministrator“ ist in großen wie kleinen Umgebungen oft keine realistische Option.
      Das ist kein guter Grund, aber es ist der tatsächliche Grund.
    • Es gibt keine Welt, in der man sich darauf verlassen kann, dass auf dem System bereits ein Binary zum Mailversand vorhanden ist; trotzdem würde ich es nicht selbst bauen.
      In den meisten Sprachen gibt es auch dann brauchbare SMTP-Client-Bibliotheken, wenn sie nicht Teil der Standardbibliothek sind.
    • Das scheint mir auch ein Fall von Zawinskis Gesetz zu sein.
  • Hier fallen zwei große schlechte Angewohnheiten auf
    Erstens, wie viele bereits angemerkt haben: Standards nicht schlampig implementieren
    Wenn man sie selbst implementieren muss, sollte man die nötige Sorgfalt und Aufmerksamkeit investieren – oder eine bereits vorhandene Library verwenden
    Zweitens: Dependencies nicht vendoren
    Die verwendeten Libraries sollte man regelmäßig und rechtzeitig aktualisieren, nicht nur „bei Bedarf“
    Wenn Updates verzögert oder ganz vermieden werden, können selbst Bugs, die upstream längst behoben sind, für Leute zu einem großen Problem werden, die meinen, man solle erst dann updaten, wenn man selbst ein Problem sieht

    • Die Alternative, Dependencies nicht zu vendoren, wirkt für mich schlechter
      Dann ist die Stabilität der Anwendung upstream-Änderungen ausgesetzt, und diese Änderungen können den Code kaputtmachen
      Man bekommt Fixes vielleicht nicht sofort, aber ich finde es besser zu wissen, dass ich wegen eines Fixes ändere, den ich brauche, statt unerwünschte Instabilität und zusätzliche Risiken hereinzuholen
      Ich bin eher bei „Wenn es nicht kaputt ist, repariere es nicht“
  • Heutzutage lernen viele Leute anscheinend die zugrunde liegenden Protokolle nicht mehr, indem sie direkt im Terminal damit interagieren
    SMTP scheint ursprünglich genau für solche manuelle Interaktion gedacht gewesen zu sein, und aus eigener Erfahrung damit über eine gewisse Zeit hat sich diese „eine Zeile mit einem einzelnen Punkt“, die eine Nachricht beendet, dauerhaft eingebrannt
    Auch das zugehörige Escaping scheint vielen Programmierern ein fremdes Konzept zu sein
    Viele sehen die obige Situation und fragen nicht: „Was, wenn ich eine E-Mail mit einer Zeile senden möchte, die nur aus einem Punkt besteht?“ Aber eine andere große Gruppe wird diese Frage als sehr logisch und leicht verständlich empfinden

    • Es würde mich eher überraschen, wenn in den letzten 30 Jahren ein nennenswerter Anteil der Softwareentwickler auf diese Weise gelernt hätte
  • Dot-Stuffing ist nötig
    SMTP https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
    Gibt es auch in POP3 https://www.rfc-editor.org/rfc/rfc1939#page-8

  • Das erinnert mich an eine Erfahrung beim Debuggen einer Netzwerkprotokoll-Implementierung. Konkret war es AppleTalk NBP, für die Älteren
    Ich hatte alles codiert, aber meine Pakete wurden abgelehnt, also stillschweigend verworfen, während die echten Pakete der Apple-Implementierung durchgingen
    Ich hatte ein gutes und ein schlechtes Paket auf dem Bildschirm und verglich sie Byte für Byte, fand aber nichts; inklusive Prüfsumme waren sie von Anfang bis Ende exakt gleich
    Als es Zeit war, Feierabend zu machen, beschloss ich, die dummen Dinger auszudrucken, um sie mir später anzusehen, und sobald sie ausgedruckt waren, sah ich den Fehler
    Meine Version war zwei Seiten lang, die korrekte Implementierung eine Seite
    Ich hatte den Buffer vor dem Senden der Daten nicht richtig geleert. So sind mbufs eben
    Ich muss immer noch lachen, wenn ich daran denke

  • Die Zeile „We are happy to welcome you to our family.“ kommt nicht einmal in die Nähe einer Zeilenlängenbegrenzung
    Da scheint etwas anderes im Spiel zu sein, etwa dass das Ganze tatsächlich ein HTML-MIME-Anhang war
    So könnte nach ... lots of text ... dann We are happy to welcome you to our family. gestanden haben
    Aber wenn man HTML blind an Zeilengrenzen schneidet, gehen Tags kaputt

    • Das war nur ein Beispiel; sorry, dass ich kein echtes Beispiel mit der exakten Zeichenzahl liefern konnte
    • Die meisten Leute, die eine kaputte HTML-E-Mail bekommen, merken einfach, dass das E-Mail-Format seltsam ist, stufen die Kompetenz des Unternehmens herab, weil es nicht einmal eine gut lesbare E-Mail schreiben kann, und gehen zur nächsten E-Mail über
      Das Unternehmen wird dieses kleine Problem sehr wahrscheinlich nie bemerken
    • Mit weichen Zeilenumbrüchen im quoted-printable-Encoding kann man Zeilen blind umbrechen, ohne HTML-Tags zu zerstören
      quoted-printable sollte inklusive CRLF eine Grenze von maximal 78 Zeichen einhalten, aber E-Mail-Clients sind meist nachsichtig
  • Wer Dot-Stuffing zum ersten Mal hört, wird kaum glauben, welche anderen Schrecken es in der Welt der E-Mail gibt
    Header-Folding, Quotes im Local Part, IPv6-Literale und Ähnliches

    • Ich frage mich, was an IPv6-Literalen problematisch ist
  • Sobald ich las, dass es SMTP-Client-Code war, der aus einem früheren Projekt übernommen wurde, erwartete ich schon das Ende, dass ein anderes Team diesen Bug noch nicht gepatcht hatte