Der merkwürdige Fall des verschwundenen Punkts
(tjaart.substack.com)- 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
- Die maximale Gesamtlänge einer Textzeile beträgt einschließlich
- 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
$2700statt$27.00aussah - 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
Ich glaube, im Titel wurde
periodnicht als Punkt, sondern als Zeitraum verstanden, haha.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.
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 =+$userDataist 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.
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.
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.
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.
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!“
Ich verstehe nicht, warum ein Cronjob, der E-Mails verschickt, einen eigenen SMTP-Client implementieren muss.
Man kann einfach ein Programm wie
mailaus 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
MAILTOin der crontab ändern.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.
In den meisten Sprachen gibt es auch dann brauchbare SMTP-Client-Bibliotheken, wenn sie nicht Teil der Standardbibliothek sind.
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
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
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 ...dannWe are happy to welcome you to our family.gestanden habenAber wenn man HTML blind an Zeilengrenzen schneidet, gehen Tags kaputt
Das Unternehmen wird dieses kleine Problem sehr wahrscheinlich nie bemerken
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
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