[RFC] Base64 URL-sicher vs. Standard-Base64
Standard-Base64 und URL-sicheres Base64 sehen fast identisch aus, brechen aber den Decoder des jeweils anderen. Hier steht, was genau sich ändert, warum, und welche Variante Sie für JWT, OAuth und URLs brauchen.
// TL;DR
Es gibt zwei Base64-Alphabete, beide spezifiziert in RFC 4648.
• Standard-Base64 (§4) — Zeichen A–Z a–z 0–9 + /, mit =-Padding. Sicher für MIME, PEM-Dateien, HTTP Basic Auth, E-Mail. Bricht, wenn man es in URLs, Cookies, Dateinamen oder DNS-Labels einsetzt.
• URL-sicheres Base64, manchmal als base64url geschrieben (§5) — Zeichen A–Z a–z 0–9 - _, Padding in der Regel weggelassen. Sicher für URLs, JWT-Teile, OAuth-State-Tokens, Cookies, DNS, Dateinamen.
Decoder für das eine Alphabet lehnen die Ausgabe des anderen ab. Wählen Sie die Variante, die zu Ihrem Transport passt, und bleiben Sie dabei.
// DIE BEIDEN ALPHABETE IM DIREKTEN VERGLEICH
// Position 62 and 63 are the only differences
//
// Standard (RFC 4648 §4) URL-safe (RFC 4648 §5)
// 0–25 A-Z A-Z
// 26–51 a-z a-z
// 52–61 0-9 0-9
// 62 + - (hyphen)
// 63 / _ (underscore)
// pad = = or omitted
// Worked example: encoding 0xFB 0xFF 0xBF
// binary: 11111011 11111111 10111111
// 6-bit: 111110 111111 111110 111111
// values: 62 63 62 63
// Standard: + / + / → '+/+/'
// URL-safe: - _ - _ → '-_-_'
// WARUM WIR EINE URL-SICHERE VARIANTE BRAUCHEN
Das Standardalphabet wurde für MIME-Bodies entworfen, die großzügig sind. URLs sind es nicht. In RFC 3986 dekodiert + mitunter zu einem Leerzeichen (geerbt von HTML-Formularen), / ist ein Pfadtrenner und = ein reserviertes Sub-Delimiter. Packt man einen Standard-Base64-String mit allen drei Zeichen in eine URL, werden verschiedene Parser unterschiedlich interpretieren, was er bedeutet. Prozentkodierung behebt das technisch (+ → %2B, / → %2F, = → %3D), doch das Ergebnis ist aufgebläht und hässlich.
RFC 4648 §5 löst das Problem, indem zwei ASCII-Zeichen gewählt werden, die in URLs keine besondere Bedeutung haben: - (Bindestrich) und _ (Unterstrich). Beide sind laut RFC 3986 unreserviert und überstehen jeden URL-Parser unversehrt. Das Padding = ist in manchen URL-Kontexten ebenfalls problematisch (es ist der Schlüssel/Wert-Trenner in Query-Strings), weshalb base64url es konventionell weglässt.
// WANN WAS
- > Standard-Base64 — verwenden für:
- > • MIME-E-Mail-Anhänge (RFC 2045)
- > • Zertifikate und Schlüssel im PEM-Format (RFC 7468)
-
>
• HTTP-Basic-Auth-Header (
Authorization: Basic ...) -
>
• data:-URIs (
data:image/png;base64,...) — die Zeichen+/=sind alle zulässig innerhalb von quotierten Attributen - > • XML-DSIG- und XML-ENC-Signaturen
- > • S/MIME
- > URL-sicheres Base64 — verwenden für:
- > • JWT-Header, -Payloads und -Signaturen (RFC 7515 schreibt base64url vor)
- > • OAuth 2.0 code_challenge / PKCE (RFC 7636)
-
>
• OpenID Connect
state- undnonce-Tokens - > • Magic Links, Einladungscodes und kurze URL-Tokens
- > • Cookie-Werte, die das Kopieren in eine URL überstehen müssen
-
>
• Aus Hashes erzeugte Dateinamen (vermeiden Sie
/in Dateinamen!) - > • DNS-Labels und TXT-Records (Bindestrich erlaubt, Schrägstrich nicht)
- > • Schlüssel in inhaltsadressierten Speichern (IPFS-ähnliche Systeme)
// UMWANDELN ZWISCHEN DEN BEIDEN
Da sich die beiden Alphabete nur an den Positionen 62, 63 und beim Padding unterscheiden, ist die Umwandlung ein Drei-Zeichen-Suchen-und-Ersetzen. Hier finden Sie sie für jede gängige Laufzeitumgebung.
// JavaScript (browser + Node)
const toUrlSafe = (b64) =>
b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromUrlSafe = (b64u) => {
let s = b64u.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '='; // re-add padding
return s;
};
// Node 16+ native
const url = Buffer.from(data).toString('base64url');
const std = Buffer.from(url, 'base64url').toString('base64');
// Python
import base64
url = base64.urlsafe_b64encode(data).rstrip(b'=') # no padding
std = base64.b64encode(data)
// Go
import "encoding/base64"
url := base64.RawURLEncoding.EncodeToString(data) // no padding
std := base64.StdEncoding.EncodeToString(data)
// PHP
$url = rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$std = base64_encode($data);
// Java
String url = Base64.getUrlEncoder().withoutPadding().encodeToString(data);
String std = Base64.getEncoder().encodeToString(data);
// Shell (GNU coreutils 9+)
echo -n 'hello' | base64 --wrap=0 | tr '+/' '-_' | tr -d '='
// DIE PADDING-FRAGE
Standard-Base64 füllt die Ausgabe mit = stets auf ein Vielfaches von 4 Zeichen auf. URL-sicheres Base64 lässt das Padding in der Regel weg (RFC 4648 §5 weist dies ausdrücklich als optional aus). JWT (RFC 7515) verbietet Padding vollständig — eyJhbGciOiJIUzI1NiJ9 hat kein nachgestelltes =. OAuth-Code-Challenges (RFC 7636) verbieten Padding ebenfalls.
Decoder müssen beide Fälle bewältigen. Ein nachsichtiger Decoder entfernt jedes nachstehende = und füllt intern auf, falls die Payload-Länge kein Vielfaches von 4 ist. Ein strikter Decoder lehnt alles ab, was nicht bereits 4-ausgerichtet ist. Wenn Sie Encoder schreiben, passen Sie sich der Erwartung des Empfängers an. Wenn Sie Decoder schreiben, seien Sie großzügig.
Wie viel Padding füllen Sie beim Dekodieren eines unpadded base64url wieder auf? Die Regel lautet: ist die Eingabelänge modulo 4 gleich 2, fügen Sie zwei = an; ist sie gleich 3, fügen Sie ein = an; ist sie gleich 0, nichts; ist sie gleich 1, ist die Eingabe ungültig (kein gültiges Base64 hat diese Länge modulo 4).
// Restore padding before decoding a JWT segment
function addPadding(b64u) {
const mod = b64u.length % 4;
if (mod === 2) return b64u + '==';
if (mod === 3) return b64u + '=';
if (mod === 0) return b64u;
throw new Error('Invalid base64url length');
}
// WAS KAPUTTGEHT, WENN SIE DIE VARIANTEN MISCHEN
-
>
Stille Datenbeschädigung — ein Standard-Decoder, der eine URL-sichere Payload erhält, dekodiert die meisten Zeichen korrekt, doch
-und_gehören nicht zu seinem Alphabet. Je nach Implementierung wirft er eine Exception, behandelt sie als Müll (überspringt sie) oder erzeugt stillschweigend falsche Bytes. - > Längen-Mismatches — der Decoder sieht einen String, dessen Länge kein Vielfaches von 4 ist (weil Padding entfernt wurde), und lehnt ihn rundheraus ab. Häufiger Schmerzpunkt bei JWT-Bibliotheken, die gepaddete Eingaben erwarten.
-
>
Prozentkodierungs-Überraschungen — wenn Sie versehentlich Standard-Base64 in eine URL setzen und eine Middleware nur
+und=prozentkodiert, haben Sie nun eine Payload mit gemischter Kodierung: gültig für manche Decoder, Müll für andere. -
>
Cookie-Abweisungen — mehrere HTTP-Server lehnen Cookies ab, die
/oder=im Wert enthalten, obwohl die Spezifikation das streng genommen erlaubt. URL-sicheres Base64 umgeht dies komplett. -
>
Dateinamen-Ausfälle unter Windows — die Standard-Base64-Ausgabe enthält
/, das Pfadtrenner ist. Eine Datei nach ihrem SHA-256 in Standard-Base64 zu benennen, scheitert unter Windows und POSIX gleichermaßen. -
>
Copy-Paste-Schäden — manche Terminals und Chat-Apps umbrechen Wörter an
/, wodurch ein Standard-Base64-String unsichtbar auf mehrere Zeilen verteilt wird. URL-sicher löst dies nicht aus.
// FALLSTUDIE AUS DER PRAXIS: JWT
Ein JSON Web Token sieht aus wie header.payload.signature, drei base64url-Strings, getrennt durch Punkte. Jede große JWT-Bibliothek (jose, jsonwebtoken, PyJWT, golang-jwt, java-jwt) kodiert mit base64url ohne Padding, weil die Tokens routinemäßig in URLs und HTTP-Headern landen.
Wenn Sie ein JWT von Hand im Browser mit atob() dekodieren, müssen Sie zuerst umwandeln: - durch + ersetzen, _ durch / und fehlende = ergänzen. Bibliotheken wie jose erledigen das intern.
// Decode a JWT payload in the browser
function decodeJwt(token) {
const [, payloadB64u] = token.split('.');
let b64 = payloadB64u.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
const json = atob(b64);
return JSON.parse(json);
}
// Example JWT payload
decodeJwt('eyJhbGciOiJIUzI1NiJ9' +
'.eyJzdWIiOiJhYmMxMjMifQ' +
'.sig').sub; // 'abc123'
// DATA-URIS: EIN SONDERFALL
Data-URIs (data:image/png;base64,...) sind technisch URLs, akzeptieren gemäß RFC 2397 jedoch das volle Standard-Base64-Alphabet einschließlich + und /. Deshalb sehen Sie überall Standard-Base64 in Data-URIs — so sieht es die Spezifikation vor.
URL-sicheres Base64 in einer Data-URI verletzt RFC 2397 technisch und kann von strikten Parsern abgelehnt werden. Halten Sie sich bei Data-URIs an Standard-Base64. Der einzige Fall, in dem URL-sicher für Data-URIs eine Rolle spielt, ist, wenn die URI selbst als Query-Parameter in eine weitere URL eingebettet wird — dann sollten Sie das Ganze stattdessen prozentkodieren.
// BASE64URL IST NICHT URL-KODIERUNG
Wert, es explizit zu sagen, denn die Namen sind verwirrend ähnlich. URL-Kodierung (auch Prozentkodierung, RFC 3986) wandelt reservierte Zeichen in %XX-Sequenzen um — das passiert mit einem Leerzeichen, wenn Sie es in die Adresszeile eines Browsers eintippen (%20).
URL-sicheres Base64 ist ein Base64-Geschmack, der so gestaltet ist, dass er keine URL-Kodierung braucht. Er erzeugt eine Ausgabe, die bereits URL-sicher ist, sodass keine Prozentkodierung nötig ist. Sie wenden niemals beides hintereinander an; Sie verwenden das eine oder das andere, je nach Daten.
Faustregel: sind die Daten binär (ein Bild, ein Hash, eine Signatur) und sollen in eine URL, nutzen Sie base64url. Sind die Daten bereits Text (ein Query-Parameter, ein Formularfeld), nutzen Sie Prozentkodierung.
// ENTSCHEIDUNGSBAUM
- > Landet der Base64-String in einer URL, einem Cookie-Wert, einem Dateinamen, einem DNS-Label, einem JWT oder einem OAuth-Flow? → URL-sicheres Base64.
- > Landet er in einem E-Mail-Body, einer PEM-Datei, einem HTTP-Basic-Auth-Header, einer Data-URI oder einem MIME-Multipart-Body? → Standard-Base64.
- > Ist der Konsument eine Bibliothek, die Sie nicht kontrollieren (JWT-Parser, OAuth-Provider, S/MIME-Client)? → Richten Sie sich nach deren Erwartung; lesen Sie die Spezifikation oder die Bibliotheksdokumentation.
- > Unsicher? → Nehmen Sie URL-sicher ohne Padding. Das ist die portabelste Variante; für strikte Empfänger können Sie jederzeit Padding hinzufügen.
// SELBST AUSPROBIEREN
Unser Base64-Encoder gibt standardmäßig Standard-Base64 aus und bietet einen Umschalter für URL-sichere Ausgabe. Unser base64url-Encoder liefert standardmäßig URL-sicher ohne Padding. Nutzen Sie beide nebeneinander, um genau zu sehen, welche Zeichen sich ändern.
Weiterführende Lektüre:
• Was ist Base64 und wie funktioniert es? — die 6-Bit-Gruppierung, die beiden Varianten zugrunde liegt.
• Base64 in JavaScript (Browser + Node) — laufzeitspezifische Helfer.
• Base64 vs. Base64URL vs. Base32 — wann keine der Base64-Varianten die richtige Wahl ist.