RFC 9839 und fehlerhaftes Unicode
(tbray.org)- RFC 9839 definiert klar, welche problematischen Unicode-Zeichen bei der Softwareentwicklung in Textfeldern enthalten sein können
- Das RFC behandelt Probleme, die aus der mangelnden Konsistenz bei der Verarbeitung dieser Zeichen in verschiedenen Sprachen und Bibliotheken entstehen
- RFC 9839 schlägt drei weniger problematische Teilmengen vor, die optional genutzt werden können
- Im Vergleich zum bestehenden PRECIS-Framework ist die Anwendung einfacher und unkomplizierter
- Zusammen mit RFC 9839 wurde auch eine Go-Sprachbibliothek veröffentlicht, die die praktische Nutzung erleichtert
Hintergrund und Überblick über RFC 9839
- Unicode wird als Standard für nahezu jede Verarbeitung von Textdaten verwendet
- Wenn man bei der Gestaltung realer Datenstrukturen oder Protokolle jedoch alle Unicode-Zeichen zulässt, entstehen Probleme
- Paul Hoffman und der Autor reichten bei der IETF einen individuellen Entwurf ein, um klare Kriterien für immer wiederkehrende Unicode-Probleme zu schaffen
- Nach zwei Jahren Diskussion wurde er als offizieller Standard angenommen und als RFC 9839 veröffentlicht
- Das Dokument erläutert detailliert Arten problematischer Zeichen, warum sie problematisch sind (aus technischen und normativen Gründen) und drei Teilmengen, aus denen Nutzer wählen können
Die wichtigsten Inhalte von RFC 9839
- Es ist ein Dokument, das bei der Gestaltung von Textfeldern in Software- und Netzwerkumgebungen unbedingt berücksichtigt werden sollte
- RFC 9839 umfasst 10 Seiten und ist damit unter den IETF-Standarddokumenten eher knapp gehalten
- Es ist vor allem für Softwareentwickler und Netzwerkingenieure leicht verständlich erklärt
Beispiele für problematische Unicode-Zeichen
- Beispielsweise könnte im JSON-Feld
usernamefolgende Zeichenkette stehen{ "username": "\u0000\u0089\uDEAD\uD9BF\uDFFF" } - Die Probleme der einzelnen Codepoints
U+0000: ein bedeutungsloses NULL-Zeichen, das das Verhalten einiger Programmiersprachen stören kannU+0089: ein C1-Steuerzeichen (CHARACTER TABULATION WITH JUSTIFICATION), dessen Verhalten komplex und uneinheitlich istU+DEAD: ein nicht gepaartes Surrogat-Zeichen, ein Problem, das aus den Grenzen von UTF-16 stammt. Dadurch entstehen nicht ideale Daten\uD9BF\uDFFF(tatsächlichU+7FFFF) : ein Noncharacter, dessen Austausch laut Standard verboten ist
- Solche Codepoints führen innerhalb von Datenstrukturen und Protokollen zu inkonsistenter Verarbeitung und unerwarteten Fehlern
- RFC 9839 definiert solche problematischen Zeichen offiziell und legt klar fest, welche Typen ausgeschlossen werden sollen
Design und Grenzen von JSON
- Das ist nicht die Verantwortung des JSON-Erfinders Doug Crockford
- JSON wurde in einer Zeit entworfen, als Unicode noch nicht ausreichend ausgereift war, weshalb der Zeichensatz nicht strikt eingeschränkt werden konnte
- Da sich der Standard heute nicht mehr ändern lässt, wird ein empirischer Ansatz zum Ausschluss problematischer Zeichen benötigt
Unterschiede zum PRECIS-Framework der IETF
- Schon vor RFC 9839 von 2025 bot die IETF verschiedene Standards an, darunter RFC 8264 (PRECIS Framework)
- Dieses Framework behandelt ausführlich, wie internationalisierte Zeichenketten aufbereitet, angewendet und verglichen werden
- Mit 43 Seiten ist es umfassend in Hintergrund und Lösungsansätzen
- PRECIS hängt stark von Unicode-Versionen ab und hat den Nachteil, komplex und schwer anzuwenden zu sein
- RFC 9839 ist knapp und praxisorientiert angelegt und lässt sich bei der Definition neuer Protokolle schnell übernehmen
Teilmengen von RFC 9839 und Anwendungsbeispiele
- RFC 9839 stellt drei realistische Teilmengen vor: scalars, XML und assignables
- Jede Teilmenge unterscheidet sich leicht im Bereich der auszuschließenden problematischen Zeichen
- Nachfolgend eine Zusammenfassung der Tabelle dazu, wie wichtige Datenformate und die Teilmengen aus RFC 9839 mit problematischen Zeichen umgehen
- Einige Formate wie CBOR, TOML, XML, YAML schließen Surrogate oder Steuerzeichen teilweise aus
- I-JSON schließt Surrogate und Noncharacters aus
- Normales JSON, Protobufs schließen sie nicht aus
- XML, YAML schließen aufgrund ihrer Charset-Eigenschaften Noncharacters/Steuercodes nur teilweise aus
- Hinweis: XML und YAML schließen Noncharacters außerhalb der Basic Multilingual Plane nicht aus
Go-Bibliothek für RFC 9839
- Es wurde eine kleine Go-Bibliothek veröffentlicht, die Zeichenvalidierung für die drei Teilmengen aus RFC 9839 unterstützt
- Sie wurde bereits ausreichend getestet, Optimierungen laufen jedoch noch
- Tests und Feedback aus der Praxis sind willkommen
Bedeutung von RFC 9839 und Entstehungsprozess
- RFC 9839 wurde nach mehreren Feedback-Runden mit den Mitautoren und mehr als 15 Überarbeitungen des Entwurfs offiziell veröffentlicht
- Durch Diskussionen und Beiträge vieler Fachleute aus der Community entwickelte es sich zu einem deutlich ausgereifteren Dokument als die Erstfassung
- Die Mitwirkenden werden im Abschnitt „Acknowledgements“ genannt
Erfahrungen mit einer individuellen RFC-Einreichung
- RFC 9839 wurde als individual submission eingereicht
- Im Vergleich zum traditionellen Weg über eine Working Group ist der Aufwand bei Arbeit und Verfahren größer
- Verglichen mit Erfahrungen in Working Groups ist der traditionelle Weg effizienter und eher zu empfehlen
1 Kommentare
Hacker-News-Kommentar
Ich denke zwar, dass klar ist, dass bestimmte Zeichen Probleme verursachen können, aber das Worst-Case-Szenario ist für mich, wenn Designer von Datenstrukturen oder Protokollen dazu neigen, nicht beliebig alle Arten von Zeichen zuzulassen – selbst korrekt escapte nicht. Zum Beispiel sollte die Validierung von Benutzernamen meiner Ansicht nach auf einer anderen Ebene behandelt werden. Also prüfen, ob ein Benutzername kürzer als 60 Zeichen ist, keine Emojis oder Zalgo-Zeichen enthält, keine Null-Bytes usw., und in der API einen passenden Fehler zurückgeben. Ich möchte nicht, dass so etwas schon beim JSON-Parsing fehlschlägt statt bei einer vorherigen Validierung. Natürlich sind bestimmte Zeichenklassen in Benutzernamen klar ungeeignet. Aber wenn ich eine Textdatei übertrage, in der tatsächlich Tabulatoren usw. vorkommen, dann erwarte ich, dass alles, was mein UTF-8-
string-Typ in der Sprache verarbeiten kann, auch kodierbar ist. Gerade Null-Bytes haben viele Anwendungsfälle und tauchen in JSON auch tatsächlich gelegentlich auf. Wenn man aber ohnehin nur eine eingeschränkte Menge „normaler“ Unicode-Zeichen verwenden soll, dann ist es besser, dafür einen Standard zu haben, statt dass jeder seinen eigenen Mini-Standard baut. Unterm Strich wirkt die Idee gut, aber die Argumentation im Blogpost überzeugt mich nicht besondersStand 2025 halte ich bei Low-Level-Wire-Protokollen im Grunde nur die folgenden Darstellungen für Strings für praktisch vertretbar
Ganz ehrlich: Ich wünschte, in Plain-Text-Dateien würden keine C0- (außer Zeilenumbrüchen und mit viel Wohlwollen HT) und C1-Zeichen verwendet. Ich verstehe, dass Leute Dinge wie ANSI-Farb-Markup speichern wollen, aber in so einem Fall ist es eben eigentlich kein Plain Text mehr, sondern eine Art Text-Markup-Format. Ähnlich wie Markdown, nur dass die Kodierung aus dem C0-Bereich verwendet wird. Nur weil sich die Daten mit dem
cat-Befehl hübsch anzeigen lassen, sind sie noch kein Plain Text. Mir ist bewusst, dass es aus Interoperabilitätsgründen viele in Plain Text kodierte Markup-Formate gibtDie Ansicht, es sei das Schlimmste, in Datenstrukturen und Protokollen beliebige Zeichengruppen zu verbieten, ist meiner Meinung nach ziemlich realitätsfern. Wirklich schlimm sind Sicherheitsverletzungen durch Softwarefehler wie Parser-Bugs
Ich frage mich, ob es überhaupt Systeme gibt, die UTF-8 in Benutzernamen zulassen. Alle Identifikatoren, die programmatisch verarbeitet oder ausgewertet werden (Login-Benutzernamen, Passwörter usw.), sollten selbstverständlich ASCII sein. Nicht ISO-8859-1, sondern ausschließlich ASCII. Unicode ist für solche Zwecke ungeeignet. Wenn ein Benutzername nur angezeigt wird, ist das etwas anderes, aber als Identifikator für System-Logins sollte jede Nicht-ASCII-Kodierung strikt verboten sein. Nicht einmal Tastatursoftware kann außerhalb von ASCII die Konsistenz der visuellen Darstellung von UTF-8 zuverlässig garantieren, und je nach Betriebssystem und Konfiguration wird es noch chaotischer. Es gibt auch keine Garantie, dass künftig erhaltene Binärdaten und Unicode interpretierende KI dieselbe Interpretation haben. Außerdem ist weder in RFC 9839 noch im Artikel klar, ob Dinge wie IVS oder Normalisierung (NFC/NFD/NFKC/NFKD) ausdrücklich in oder außerhalb des Scopes liegen. Ein Abschnitt zum Zweck scheint völlig zu fehlen. Es gibt nur vage Aussagen in der Art, dass es „Nichtzeichen-Codepunkte“ gibt
Ich frage mich, warum man Emojis in Benutzernamen verbieten sollte
Ich möchte nur anmerken, dass die IETF nicht bis 2025 gewartet hat, um Bad Unicode zu behandeln. Schon früher wurde in RFC 8264: PRECIS Framework eine breite Palette solcher Unicode-Probleme ausführlich behandelt. Auch die zugehörigen RFCs 8265 (Link), 8266 (Link) usw. sind hilfreich. Generell sollten Dinge wie Passwörter, die die Textrichtung verändern oder je nach Eingabegerät unterschiedlich kodiert werden können, nicht in Benutzernamen/Passwörtern verwendet werden. Mit solchen RFC-Profilen lässt sich das sicher handhaben. Für diesen Zweck ist „failing closed“ (strikteres Blockieren) sicherer. Selbst wenn neue Emojis erscheinen, ist es mir lieber, sie in Benutzernamen vorsorglich zu verbieten, statt sie zu erlauben und damit Auswirkungen auf alle Seiten zu riskieren
Unicode hat eindeutig „gute“ Seiten, aber es ist frustrierend, dass man wissen muss, welche Zeichen ausnahmsweise ausgeschlossen werden sollten. Das ist das Ergebnis des Versuchs, Schriftsysteme umfassend abzubilden, wodurch alles zu komplex geworden ist. Es ist ermüdend, ständig im Kopf behalten zu müssen, welche Zeichen gesondert behandelt werden müssen. Deshalb behandle ich Unicode-Strings als eigenständige Dateneinheit. Ich nehme sie entgegen, speichere sie, rendere sie und vergleiche sie auf Gleichheit, aber ich versuche nicht, ihren Inhalt zu interpretieren. Sogar das Verketten oder anderweitige Verarbeiten von Strings fühlt sich unsicher an
Unicode ist wie ein endloser Abgrund aus Trivia und schlechten Entscheidungen. Zum Beispiel warnen die betreffenden RFCs vor veralteten ASCII-Steuerzeichen (wegen möglicher Anzeigeverwirrung), erwähnen aber Richtungssteuerzeichen mit gravierenden Sicherheitsproblemen wie Explicit Directional Overrides mit keinem Wort
Ein einfaches Beispiel: Wenn der erste String mit einem verwaisten Emoji-Modifikator endet und der zweite mit einem modifizierbaren Emoji beginnt, hat man bereits ein Problem. Und je mehr komplexe Fälle hinzukommen, desto schlimmer wird es nur
Die Komplexität ist groß, aber Dinge wie Surrogates und Steuerzeichen sind dabei nicht für die Aufzeichnung von Sprache gedacht, sondern Überbleibsel seltsamer historischer Designentscheidungen
Unicode ist zwar unbequem, aber immer noch weniger unbequem als die früheren Kodierungsstandards
Ich denke, die meisten Probleme lassen sich lösen, indem man ungültige UTF-8-Bytefolgen ablehnt oder generell als Fehler behandelt. Zum Beispiel sind Surrogates in UTF-8 ohnehin illegal, also sollte eine Sprache mit UTF-8 für solche Sequenzen Fehler zurückgeben. Das eigentliche Problem sind meiner Ansicht nach eher problematische „Codepunkte“ (nicht druckbare usw.). Das ist klar ein anderes Konzept als illegale Bytefolgen und sollte auch getrennt behandelt werden, damit es nützlicher ist
Unicode definiert bereits Kategorien (General Category) für jeden Codepunkt, um ungewöhnliche Zeichentypen einzuordnen. Siehe den entsprechenden Wikipedia-Artikel. In Python liefert zum Beispiel
unicodedata.category(chr(0))„Cc“ (control) undunicodedata.category(chr(0xdead))„Cs“ (surrogate)Ich halte es für übertrieben, alle „legacy control“-Zeichen nicht nur als Literale, sondern auch in escapten Strings (z. B.
"\\u0027") auszuschließen. C1 wird selten benutzt, daher ist das in Ordnung, aber für einige C0-Zeichen gibt es reale Anwendungsfälle. Escape, EOF, NUL usw. haben meiner Meinung nach weiterhin einen klaren NutzenEinige eher ungewöhnliche C0-Zeichen (wie U+001E Record Separator) sind in Datenströmen meiner Meinung nach sehr nützlich. In Dokumenten kann man sie verbieten, aber in Streaming-Daten sind sie brauchbar
Ich habe form feed (U+000C) schon in Programmquellcode gesehen. Emacs unterstützt das traditionell für Navigation nach Seiten, daher kommt so etwas vor
Ich halte Unicode nicht für gut. Unabhängig vom Zeichensatz muss jede Anwendung letztlich selbst festlegen, welche Arten von Zeichen tatsächlich verwendet werden dürfen (Steuerzeichen, grafische Zeichen, maximale Länge usw.). Ein- oder Ausschluss in JSON und Ähnlichem bringt nicht viel. Ob Unicode, ASCII oder ein anderer Zeichensatz: Manchmal kann es nützlich sein, bestimmte Subsets (oder Supersets) zu benennen, aber man sollte nicht glauben, dass das für alle die richtige Wahl ist. RFC 9839 gibt einigen Unicode-Subsets Namen, aber das bedeutet nicht, dass sie für meinen Dienst automatisch richtig sind. Mein Fazit ist, dass man sogar erwägen sollte, Unicode gar nicht zu verwenden oder es nicht zu erzwingen
Ich überlege, ob man die Eingabe kontrollieren oder sie stattdessen in einen Datentyp kapseln sollte, der für nicht vertrauenswürdige Eingaben sicher ausgegeben werden kann (für Web, Logs und Debugging)
Ich wünschte, es gäbe im Standard eine Begrenzung dafür, wie viele Unicode-Skalarwerte in eine grafische Einheit passen dürfen. Als ich das zuletzt geprüft habe (vor ein paar Jahren), gab es keine solche Grenze; stattdessen wurde für Streaming-Anwendungen nur empfohlen, grafische Einheiten auf 128 Byte zu begrenzen. Wenn solche Grenzen im Standard klar festgelegt wären, würde das Implementierungen stark vereinfachen und unnötige Einschränkungen vermeiden
Ich bin tatsächlich schon auf Fälle gestoßen, in denen Programme mit der Annahme „es gibt keine Steuerzeichen“ kaputtgingen (form feed für Seitenwechsel, Escape-Zeichen für Terminals usw. werden durchaus verwendet). Auch die Annahme „alles ist UTF-8“ bricht gelegentlich zusammen (alte Datendateien, Logs usw.). Wenn man den Text nicht inhaltlich sinnvoll verarbeitet, ist es am besten, ihn einfach als Bytefolge unverändert weiterzureichen. Aber wegen Microsoft Windows muss man manchmal doch eine
char16_t-Sequenz übergeben. UTF-16 unterscheidet sich bei Ein- und Ausgabe grundlegend von UTF-8. Bei Konvertierungen sollte man für externe Daten → internes Format jeweils WTF-8 (UTF-16) bzw. Surrogate-Escape (UTF-8) verwenden. Die beiden Verfahren lassen sich nicht mischen