Base64 und URL-Kodierung: Zweck, Fallen, richtiger Einsatz
Zusammenfassung (TL;DR)
Rufe echo -n "Hi" | base64 auf, und du bekommst genau vier Zeichen zurück: SGk=. Zwei Eingabebytes (16 Bit) werden in 6-Bit-Stücke zerlegt, auf eine 4-Zeichen-Ausgabe aufgepolstert, und das ist die gesamte Arithmetik. Behandle diese simple Regel als Default im Sinne von „sicherheitshalber alles einpacken”, und die Kosten addieren sich rasch – drei Eingabebytes werden immer zu vier Ausgabezeichen, also rund 33 % Aufblähung – und ein 1,2 MB großes JPEG, das ich einmal sah, als ein Dienst es Base64-verpackt in ein JSON-Feld schob, landete auf der Leitung bei rund 1,64 MB. Kodierung ist weder Verschlüsselung noch Komprimierung. Sie ist ein Regelwerk, um ein Alphabet innerhalb eines anderen auszudrücken, damit Daten einen Kanal überleben, der sie sonst verstümmeln würde. Base64 (RFC 4648 §4) existiert, um beliebige Bytes durch Systeme zu transportieren, die nur Text verstehen – E-Mail-Bodies, JSON-String-Felder, Inline-Bilddaten. Base64URL (derselbe RFC, §5) tauscht zwei Zeichen (+→-, /→_), sodass das Ergebnis gefahrlos in einen URL-Pfad, einen Dateinamen oder ein JWT passt. Prozent-Kodierung (RFC 3986) ist ein separates Werkzeug: Sie wandelt Zeichen, die in URLs etwas bedeuten (?, &, #, Leerzeichen, Unicode), in %XX-Sequenzen um, die nichts bedeuten. Wähle die Kodierung passend zum Kanal: binär, wenn der Pfad es unterstützt, Base64 für Textkanäle, Base64URL für URL-eingebettete Daten und Prozent-Kodierung für alles, was bereits in einem URL-Kontext steht. Offengelegte Base64-Strings bleiben für jede Person mit Decoder lesbar, behandle Kodierung also nie als Geheimhaltungsmechanismus.
Hintergrund und Konzepte
Textreine Kanäle existierten lange, bevor jemand sie bat, Bilder zu tragen. E-Mail setzte historisch 7-Bit-ASCII voraus und verstümmelte oder strippte Bytes mit gesetztem höchsten Bit; HTTP-Header schränken bis heute gewisse Zeichen ein; JSON-String-Felder sind UTF-8, können aber keine rohen Steuerbytes (0x00–0x1F) sicher halten. Base64, in RFC 4648 standardisiert, löst das Problem, indem es jeweils 3 Eingabebytes (24 Bit) auf 4 Ausgabezeichen zu je 6 Bit abbildet. Das Alphabet besteht aus 64 Zeichen: A-Z (Indizes 0–25), a-z (26–51), 0-9 (52–61), + (62), / (63), mit = als Padding, um die Ausgabelänge auf ein Vielfaches von vier zu bringen. Ein Eingabebyte erzeugt XX== (8 Bit → 6+2 gepadded), zwei Bytes ergeben XXX= (16 Bit → 6+6+4 gepadded), und drei Bytes ergeben XXXX ohne Padding.
Die URL-sichere Base64-Variante ändert nur die Alphabetindizes 62 und 63 – + wird zu -, / wird zu _ – weil + in Legacy-Formularabsendungen als Leerzeichen dekodiert wird und / ein Pfadtrenner ist. In vielen URL-sicheren Kontexten wird auch das =-Padding weggelassen; das ist die Form, die in JSON Web Tokens (RFC 7519) verwendet wird, wo ein kompaktes header.payload.signature-Triple ohne erneutes Escapen in einen Authorization: Bearer ...-Header passen muss.
Prozent-Kodierung (manchmal URL-Kodierung genannt) ist etwas Separates. RFC 3986 definiert zwei Kategorien von ASCII: unreserved-Zeichen (A-Z, a-z, 0-9, -, ., _, ~), die unverändert erscheinen dürfen, und reserved-Zeichen (:, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =), die strukturell etwas bedeuten und kodiert werden müssen, wenn sie als Daten statt als Syntax auftreten. Jedes Byte außerhalb dieser Menge – einschließlich der Bytes eines UTF-8-kodierten Nicht-ASCII-Zeichens – wird in Hexadezimal als %XX geschrieben. encodeURIComponent in JavaScript kodiert sowohl reserved- als auch non-reserved-Zeichen aggressiv für den Einsatz innerhalb einer Komponente; encodeURI ist großzügiger, weil es davon ausgeht, dass du eine ganze URL übergibst.
Eine subtile Konsequenz dieser drei Kodierungen ist, dass sie nicht austauschbar sind, selbst wenn sich die produzierten Bytes überlappen. Ein prozent-kodierter Query-Parameter besteht immer noch aus ASCII-Zeichen, die andere ASCII-Zeichen kodieren; ihn durch einen Base64-Decoder zu schicken, liefert Schrott. Ein Standard-Base64-String, in einen URL-Pfad gelegt, bricht leise, weil das darin enthaltene / als Pfadtrenner gelesen wird – ich habe zugesehen, wie ein Kollege wegen eines einzelnen solchen Zeichens einen halben Tag verlor. Diese Vermischungen sind die Quelle der Hälfte aller Kodierungsbugs in Produktion.
Vergleich und Daten
| Eigenschaft | Base64 (Standard) | Base64URL | Prozent-Kodierung |
|---|---|---|---|
| Wann verwenden | Textkanal, der rohes Binär ablehnt (E-Mail MIME, JSON-String aus Bytes) | Dasselbe, aber in URL, Dateiname oder JWT eingebettet | Eine einzelne URL-Komponente mit reserved- oder Nicht-ASCII-Zeichen |
| Alphabet | A–Z a–z 0–9 + / = | A–Z a–z 0–9 - _ (Padding optional) | Unreserved-Menge; alles andere wird zu %XX |
| Overhead | Rund 33 % (4 Zeichen je 3 Bytes), plus 76-Zeichen-Zeilenumbruch in MIME | Rund 33 %, typischerweise ohne Padding | Variabel – 1 UTF-8-Byte wird zu 3 ASCII-Bytes (%XX) |
| Bricht bei | Rohem + und / in URLs, fehlendem Padding in strikten Decodern | Standard-Base64-Decodern, die +/= erwarten | Doppelkodierung (bereits prozent-kodierte Daten werden erneut kodiert) |
| Typische Einsätze | MIME-Anhänge, Data-URIs, Inline-Binärdaten in JSON | JWT, URL-sichere IDs, Kurzlinks | Query-Parameter, Pfadsegmente, Formular-Bodies |
Die Zahlen sind für die Budgetierung wichtig. Ein 1 MB großes Bild wird Base64-kodiert zu rund 1,37 MB; bei MIMEs 76-Zeichen-Zeilenumbruch kommen nochmal etwa 2 % hinzu. In einer HTTP-Antwort trifft diese Aufblähung sowohl Server als auch den Parser der Clients. Prozent-Kodierung ist auf Stringebene meist ein kleineres Thema, kann aber Bytes bei CJK-Text vervielfachen: Ein koreanisches Zeichen belegt in UTF-8 3 Bytes, was nach der Kodierung 9 ASCII-Zeichen ergibt. Das zweizeichige Wort „안녕” wird innerhalb einer URL zur 18-Byte-Sequenz %EC%95%88%EB%85%95.
Praxisszenarien
Szenario 1 – E-Mail-Anhänge. Eine PDF reist in einem MIME-Teil mit Content-Transfer-Encoding: base64. Der Mail-Client Base64-kodiert die Datei, bricht Zeilen bei 76 Zeichen (dem klassischen MIME-Limit) um und sendet sie. Der Empfänger kehrt den Vorgang um. Die textreine Annahme von SMTP machte Base64 hier lange vor modernen Erweiterungen wie 8BITMIME oder BINARYMIME zum Default, und die meisten Mailserver emittieren weiterhin sicherheitshalber Base64. Der 33-%-Overhead wird als Kostenpunkt verlässlicher Zustellung akzeptiert.
Szenario 2 – JSON Web Tokens. Ein JWT (RFC 7519) ist header.payload.signature, wobei jedes Segment Base64URL-kodiertes JSON oder Bytes ist, ohne Padding. Das URL-sichere Alphabet bedeutet, dass Tokens in Authorization-Headern, access_token-Query-Parametern und Logzeilen ohne erneutes Escapen auftauchen können. Das Fehlen des Paddings hält sie kurz. Eine Warnung: Wer ein JWT dekodiert, kann die Claims lesen – die Kodierung ist keine Sicherheit; die HMAC- oder RSA-Signatur ist es. Keine Geheimnisse in die Payload legen.
Szenario 3 – Data-URIs für kleine Bilder. Ein background-image: url("data:image/png;base64,iVBORw0K...") bettet eine PNG direkt ins CSS ein. Das spart einen Round-Trip für winzige Assets wie Icons. Doch der 33-%-Overhead von Base64 und der Verlust von Browser-Caching und parallelen Requests bedeuten, dass es nur für Assets gewinnt, die klein genug sind, sodass der zusätzliche HTTP-Round-Trip teurer gewesen wäre. Meiner Erfahrung nach liegt der Break-even bei rund 1–2 KB; darüber ist eine separate gecachte Datei oder ein SVG meist schneller.
Szenario 4 – Einen Nutzer-Avatar hochladen. Ein verbreitetes Anti-Pattern ist, eine Datei mit FileReader.readAsDataURL zu lesen – was einen data:image/png;base64,...-String liefert – und diesen String dann als JSON-Feld zu POSTen. Das funktioniert, doch es ist fast nie die richtige Form: Die Payload ist nun 33 % größer, der Server muss sie vor dem Schreiben auf Disk dekodieren, und beide Seiten verbrauchen Arbeitsspeicher für die String-Form. In einem Fall, den ich beobachtet habe, wuchs ein 5 MB großes Bild auf rund 6,7 MB JSON an und verursachte Timeouts in Mobilfunknetzen; der Wechsel zu multipart/form-data schickte dieselbe Datei mit 5 MB. Greife in diesem Ablauf nur dann zu Base64, wenn die Transportebene wirklich einen JSON-String verlangt, etwa wenn eine Drittanbieter-API keinen Multipart-Request akzeptiert.
Häufige Missverständnisse
„Base64 ist Verschlüsselung.” Ist es nicht. Die Zuordnung steht in RFC 4648, das Alphabet ist fest, und jeder Decoder gibt die Originalbytes zurück. Kodierung schützt den Transport, nicht den Inhalt. Ist die Payload sensibel, verschlüssele zuerst und packe den Ciphertext für den Transport in Base64.
„URL-Kodierung braucht man nur für Nicht-ASCII-Zeichen.” Viele reserved-ASCII-Zeichen müssen ebenfalls kodiert werden, wenn sie als Daten auftauchen. Ein Query-Wert, der ein & enthält, muss zu %26 werden, sonst parst der Server ihn als neuen Parameter. # muss zu %23 werden, sonst wird alles dahinter als Fragment behandelt. Die Regel ist strukturell, nicht zeichensatzbasiert.
„Alles, was durch HTTP geht, sollte sicherheitshalber Base64-kodiert sein.” HTTP trägt Binär-Bodies problemlos – Content-Type: application/octet-stream, Chunked Transfer, beliebige Bytewerte. Base64 ist eine Problemumgehung für Kanäle, die das nicht können, und einen 33-%-Overhead zu zahlen, wenn der Kanal Bytes längst beherrscht, ist reine Steuer. Nutze multipart/form-data oder einen rohen Body für Datei-Uploads und behalte Base64 für die Fälle vor, in denen der Kanal wirklich Text benötigt (ein JSON-Feld, ein URL-Parameter, ein MIME-Body).
„Base64 und Base64URL sind austauschbar.” Sind sie nicht. Einen URL-sicheren String in einen strikten Base64-Decoder zu schicken, der +/ erwartet, scheitert oder liefert Schrott. Bibliotheken bieten meist beides an; stimme Encoder und Decoder end-to-end ab. Nodes Buffer.from(s, 'base64') akzeptiert beide Alphabete, doch nicht jede Standardbibliothek ist so großzügig.
Checkliste
- Ist der Kanal 7-Bit-rein oder strukturiert-textuell? E-Mail-Body, JSON-String aus Bytes, CSS-
data:-URI → Base64. - Geht der kodierte Wert in eine URL, einen Dateinamen oder ein JWT? Nutze Base64URL und entscheide, ob Padding erlaubt ist.
- Ist der Wert bereits eine URL-Komponente? Prozent-kodiere, und nur die Teile, die es brauchen.
- Sind die Daten groß? Erwäge, ob der Kanal Kodierung wirklich verlangt – ein roher Binär-Body vermeidet die 33 % Overhead.
- Ist das Dekodieren streng oder nachgiebig? Stimme Standard-/URL-sichere Alphabete end-to-end ab; mische nicht.
- Ist Vertraulichkeit ein Ziel? Verschlüssele vor dem Kodieren. Kodierung allein macht Daten nie geheim.
Verwandtes Tool
Der Base64-Encoder/-Decoder von Patrache Studio behandelt Standard- und URL-sichere Varianten lokal, sodass die Eingabebytes deinen Browser nicht verlassen – nützlich, wenn die Daten ein Token oder ein kleiner Schlüssel sind. Falls die Base64-Payload ein JWT ist, kombiniere sie mit JSON formatieren, validieren und mit Schema prüfen in der Praxis, um die dekodierten Claims sauber zu inspizieren. Und wenn das kodierte Blob eine kompakte ID aus einer UUID ist, erklärt UUID v1 vs. v4 vs. v7: Einen DB-Primärschlüssel wählen, warum die ursprünglichen Bytes, die du Base64URL-kodiert hast, für Sortierreihenfolge und Indizierung weiterhin zählen.
Quellen
- IETF RFC 4648, „The Base16, Base32, and Base64 Data Encodings” — https://datatracker.ietf.org/doc/html/rfc4648
- IETF RFC 3986, „Uniform Resource Identifier (URI): Generic Syntax” — https://datatracker.ietf.org/doc/html/rfc3986
- MDN, „Base64”-Glossareintrag — https://developer.mozilla.org/en-US/docs/Glossary/Base64
- MDN, „encodeURIComponent()” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent