Stranger Than Usual

String encoding in Go

(Usually I write stuff in German, but this could be interesting to some people I know who do not speak German, so English it is.)

As frequent readers (do I have those?) know, text encoding is a small hobby / pet peeve of mine. A lot of things would be easier if we just used UTF-8 everywhere. But a consistent text encoding is only useful if it is used correctly and its validity is enforced. Most languages do a bad job at enforcing valid encoding. For example:

  • C does not have strings, C has null-terminated char-arrays. Encoding optional
  • C++ has strings that do not enforce encoding, newer versions have some questionable unicode strings
  • Java encodes strings in UTF-16 and does not check for unpaired surrogates
  • Javascript works similar to Java in that regard

Python and rust enforce valid unicode. Except that python allows to encode surrogate code points (but at least it does fail when it tries to encode that string as UTF-8). However, I'm not here to talk about any of these languages. I'm here to talk about Go, since I'm probably going to use it professionally again next week.

Strings in Go are just gloriefied immutable byte arrays. They have no inherent guarantee to contain a valid string of any encoding. Since Go is such a young language, this is a shame. The worst part: Developers don't expect it. They take a modern language, read strings from external sources, modify them, write them to external sinks and are oblivious about the fact that they are just one step away from splitting a code point in the middle.

I talked about the strengths and weaknesses of Go in another blogpost. Here, I'm going to ask the question: How bad is the situation? What can go wrong, where is easy to make mistakes and where is it hard to do so? Let's start with the basics.

Thing I already knew

Source files in Go are strictly UTF-8. So in theory, string literals are also strictly UTF-8. Unless they are not because you can use escape sequences to do stuff like this:

var s = "foo\xffbar"

There, an invalid byte in the middle of the string. Go has the unicode/utf8 package in its standard library, so we can at least check if the string is valid:

utf8.ValidString(s) // evaluates to false for the string above

You can also just create a string from a byte slice without validation. I have seen many developers doing exactly this, without even thinking about any problems:

c := []byte{0x66, 0x6f, 0x6f, 0xff}
s := string(c)

It should come to no surprise that you can just split a string in the middle of a code point:

s := "🐍"
s1 := s[:2] // s1 now contains an incomplete code point

Just as with casting a byte array to a string, this is something I have seen in the wild several times. Barely anyone thinks about it.

This situation is bad. It is far too easy to get invalidly encoded strings. And Golang is used primarily for web servers. Those get untrusted input all the time. So maybe at least the interfaces that are commonly used are protected? I have never tried this before, so let's do it!

Unicode validation at the border of golang web services

I have written a small test web server for a few test cases. I want to test three interfaces (or two, depending on how you count):

  • URL-parameters for GET requests
  • HTML form data from a POST request
  • JSON data (in this case from POST request, but it does not really matter)

The first two can count more or less as one, because the interface in Golang is the same: A http.Request-object has a field Form (this field needs to be populated by a call to ParseForm(), but that detail does not really matter here). Form is basically a map from parameter name to parameter value(s). So this is what I did:

if err := req.ParseForm(); err != nil {
    response.WriteHeader(http.StatusBadRequest)
    fmt.Fprintln(response, "bad form data")
    return
}

query, ok := req.Form["q"]
// [I left out some validation code here]
if utf8.ValidString(query[0]) {
    log.Printf("query string '%s' is valid utf-8", query[0])
} else {
    log.Println("query string is not valid utf-8")
}
response.WriteHeader(http.StatusOK)
fmt.Fprintln(response, "looks good")

So basically: I try to parse the form data and return an error if any problem occurs. Then I get the string from the form map and log whether it is valid UTF-8. Then I return with a success status code. Ideally, ParseForm() should fail for invalid UTF-8. Let's see what happens:

$ curl "http://localhost:8090/form?q=foo%ff"
looks good

Needless to say, there is a query string is not valid utf-8 message in the server log. And that was the most simple case. POST-body form data has basically the same result, no matter whether I use %ff to escape the byte or use a raw 0xff-byte in the body.

So that's form data. What about JSON? After all, we all write bloated single-page javascript apps with a server that is basically a JSON-API. Does Go's JSON parsing do it better? As a matter of fact, it does! Assume I have this struct

type Input struct {
	Query string `json:"q"`
}

and then handle it like this:

body, err := io.ReadAll(req.Body)
// [omitted error handling]

var input Input
err = json.Unmarshal(body, &input)
// [omitted error handling]

if utf8.ValidString(input.Query) {
    if strings.ContainsRune(input.Query, '�') {
        log.Printf("query string '%s' is valid utf-8 but contains a replacement character", input.Query)
    } else {
        log.Printf("query string '%s' is valid utf-8", input.Query)
    }
} else {
    log.Println("query string is not valid utf-8")
}
response.WriteHeader(http.StatusOK)
fmt.Fprintln(response, "looks good")

No matter what I threw at it, it was always handles gracefully. An escaped surrogate code point? Replaced with the replacement character. An unescaped surrogate code point? Replaced with three replacement characters. An unescaped 0xff-byte? Replaced with the replacement character. An overlong encoding of "}? Replaced with eight replacement characters.

So while I'm a bit unhappy that it does this silently and I have not found an option to let the parsing fail instead of replacing unexpected bytes, this is at least valid behaviour and leads to valid encoding.

Conclusions

Always check strings from untrusted sources for valid encoding. I cannot stress this enough: Most standard library functions will ignore encoding! You have to check manually. json.Unmarshal may have your back, but unless you know for certain that this is the case, always check (in addition to other security measures you should take with untrusted input).

Also: do not split strings in arbitrary places, do not cast byte arrays to strings without checking for valid encoding and be very careful with escape sequences in string literals.

Bahncard-Tortur

Ich sollte mal eine Statistik aufstellen, welche Tags in diesem Blog am häufigsten zusammen vorkommen. Ich würde wetten, es sind „rant“ und „deutsche bahn“.

Was ist es diesmal? Mal wieder der Onlineauftritt, in Kombination mit ihrem halbgaren Digitalzwang. Ich wollte nur eine Bahncard kaufen, weil ich demnächst häufiger mal nach Hamburg fahren muss. Mein Ärger fing schon beim Login an.

Passwortrichtlinien

Die DB hat ihre Passwortrichtlinien geändert. An sich kein Problem. Bisher wurden definitiv zu kurze Passworte erlaubt. Aber anstatt nach dem Stand der Wissenschaft zu gehen, sehe ich das hier:

For your own security, please choose a password that you have never used before. At least 12 characters, contains uppercase letters, contains lowercase letters, contains numbers, contains special characters, not your e-mail address

7 Regeln, nur drei davon sind gut: Das Passwort muss neu sein, mindestens 12 Zeichen lang, nicht die E-Mail-Adresse.

Ich habe mich dazu schon öfters ausgelassen, deswegen spare ich mir das jetzt. Siehe Umzug nach Nijmegen, Sparkasse pushTAN und das Passwort und Furchtbare Passwortrichtlinien bei der Bundesagentur für Arbeit. Manchmal frage ich mich, wer im falschen Paralleluniversum lebt. Bin ich es, und bilde mir nur ein, dass so komplizierte Passwortregeln als kontraproduktiv erwiesen wurden? Oder die Erfinder solcher Vorgaben, die die letzten zehn Jahre oder mehr verschlafen haben?

Dann war ich endlich eingelogged. Ich habe mir die Bahncard herausgesucht, „Lastschrift“ als Zahlart ausgewählt und wollte die Bestellung abschließen. Ich muss nur noch einen kleinen Schritt durchführen, sagt die Bahn-Website.

Identifizierung durch Verimi

Ich muss nur noch meine Identität bestätigen. Wo kämen wir denn hin, wenn ich für jemand anderen eine Bahncard kaufen könnte? Zur Identitätsbestimmung gibt es mehrere Möglichkeiten.

Als Erstes die E-Perso-App, oder wie die heißt. Ich habe zwar gerade einen neuen Perso bekommen, habe das aber noch nicht eingerichtet, außerdem fehlt mir der Chipkartenleser. Also fällt das erst einmal weg.

Als Zweites kann ich Verimi (oder einem anderen Drittanbieter) meine Logindaten für mein Online-Banking geben, dann können die sicher sein, dass ich ich bin. Meine Logindaten. Für Online-Banking.

Kermit der Frosch am Telefon: JA, DIE HABEN ALLE LACK GESOFFEN. NEIN, ICH WEIß NICHT WIE VIEL

Das ganze hat bei mir Flashbacks ausgelöst. Damit musste ich Silvester 2023 auch schon kämpfen, als ich mir ein Deutschlandticket besorgen wollte.

Die dritte Option war, meinen Perso zu Fotografieren und denen gleich auch noch ein Selfie zu schicken. Auch nicht meine Lieblingsvariante, insbesondere nach dieser Geschichte neulich, bei der eine Hotelkette Ausweisdaten gesammelt und nicht gesichert hat. Aber ich brauche diese Bahncard. Nach einigem Herumprobieren (Verimi war pingelig, was die fotografische Qualität meines Ausweises anging) war ich dann identifiziert.

Aber wenigstens kann ich dann endlich mit Lastschrift zahlen, oder?

Die Zahlart

„For the better, right?“ meme. Vier Panels. Panel 1: Anakin: „Du musst dich ausweisen.“ Panel 2: Padme (fröhlich): „Dann kann ich per Lastschrift zahlen, richtig?“. Panel 3: Beat Panel, Anakin schweigt. Panel 4: Padme (zweifelnd): „Richtig?“

Hahaha. Nein.

Die Zahlung konnte nicht durchgeführt werden. Die Zahlung konnte mit dem gewählten Zahlungsmittel nicht durchgeführt werden. Bitte wählen Sie ein anderes Zahlungsmittel.

Ok, dann also Paypal. Ich lese immer wieder von Leuten die schreiben, man solle Paypal nicht benutzen, weil Peter Thiel ein Arschloch ist. Ist er, aber irgendwie muss ich ja meine Bahntickets bezahlen.

Immerhin habe ich jetzt das Bahnticket. Das wird natürlich automatisch verlängert, man kann es, ähnlich wie das Deutschlandticket, nur im Abo kaufen. Zwei Mal bin ich schon darauf hereingefallen, dass ich vergessen habe, die Bahncard zu kündigen, dann habe ich eine Rechnung bekommen, die in den ganzen Ihr-Zug-kommt-zu-spät-oh-nein-doch-nicht-oh-warte-doch-ach-ne-er-fällt-komplett-aus-Spam-Emails untergegangen ist, und statt einer Mahnung haben sie mir dann direkt ein Inkassounternehmen auf den Hals gehetzt, dass eine saftige Strafgebühr verlangt hat. Vielleicht lassen sie mich deswegen nicht per Lastschrift zahlen. Ihr wisst, schon, dann würde das ja automatisch abgebucht und sie bekämen die Strafgebühr nicht.

Dass ich verdammte vier Wochen vor Ablauf kündigen muss, halte ich auch für eine Frechheit. Immerhin werde ich darauf hingewiesen. Und als weiteren Beweis, dass die Bahn unfähig ist, eine Website zu bauen, haben sie ihr HTML auch einmal zu viel escaped und zeigen statt einem Link den rohen HTML-Code an:

Eine Infobox, die die Kündigungsbedingungen der Bahncard erläutert. Darin ist HTML-Code, der wohl ein Link zur Seite mit der Widerrufsbelehrung werden sollte.

Gut, wann schicken die mir meine Bahncard zu?

Digitalzwang

Das war eine Fangfrage. Natürlich schicken die mir keine Bahncard mehr zu. Physische Bahncards haben ausgedient, das hatte ich schon vorher mitgekriegt. Digitalcourage hatte das immer mal wieder angesprochen.

Ich brauche also die DB-Navigator-App. Eine App, die wegen Datenschutz- und Sicherheitsproblemen immer mal wieder in den Schlagzeilen war. Ist aber auch irrelevant, ich kann die App nicht installieren, weil ich den Playstore nicht auf meinem Phone habe. Immerhin kann man mittlerweile ein Ersatz-PDF herunterladen, dass man dann ausdrucken oder auf einem Gerät vorzeigen kann. Aber wo finde ich das?

Es gibt eine Website mit einer laaangen Anleitung, wie man die DB-Navigator-App benutzt. Darunter ein Video, wie man die PDF-Bahncard herunterladen kann. Darunter noch eine „barrierefreie“ Option. Diese Option ist eine PDF-Datei mit vielen Bildern. 9 Seiten lang. 8 Seiten davon eine Anleitung, warum man doch lieber die App verwenden sollte. Auf Seite neun dann fünf kurze Punkte, wo man die Bahncard-PDF-Datei findet (sie ist gut versteckt). Fünf Punkte auf einer Strichpunktliste. This PDF could have been a website.

Naja, jetzt habe ich die Bahncard wenigstens. Und habe schon wieder zwei Stunden verbracht, diesen Blogpost zu schreiben. Ich hatte heute noch etwas anderes vor.

Keilschriftuhr

Eigentlich wollte ich ja gestern nichts über die Bahn schreiben, sondern über etwas Schönes. Ich habe mich ja vor Kurzem über das GRSSK-Phänomen und den Missbrauch von nicht-lateinischen Schriften ausgelassen.

Im Gegensatz dazu kann man Keilschrift aber auch korrekt einsetzen. Da hat zum Beispiel jemand eine Website gebaut, die die Uhrzeit in Keilschrift anzeigt. Bonuspunkte für den Pfadnamen sumertime.

Diese Uhr benutzt die echten Babylonischen Zahlendarstellungen. Die gehen von 1 bis 59 (und ein Leerzeichen für die 0) und sind damit perfekt geeignet, um Minuten und Sekunden darzustellen. Tatsächlich lässt sich die 60 Minuten/Stunde, 60 Sekunden/Minute schlussendlich auf das Sexagesimalsystem der Sumerer und Babylonier zurückführen, auch wenn die eigentlich Aufteilung laut Wikipedia zum ersten Mal vor etwa tausend Jahren verwendet wurde.

Auch die Doppelpunkte, um Stunden, Minuten und Sekunden zu trennen sind wahrscheinlich ein Anachronismus. Trotzdem eine schöne Uhr.