Haben Sie mit dem Base64-Format zu tun? Dann ist diese Website genau das Richtige für Sie! Nutzen Sie unser superpraktisches Online-Tool, um Ihre Daten zu kodieren oder zu dekodieren.

Base64-Dekodierung in PHP: Ein vollständiger Leitfaden

Es taucht in einem Support-Ticket, einem API-Log, einer Konfigurationsdatei oder mitten in einer URL auf: eine lange Kette aus Buchstaben und Ziffern, hin und wieder ein + oder /, und vielleicht ein = oder zwei am Ende. Sie erkennen es im Handumdrehen. Base64 ist ein Format von Binärdaten zu Text: Es schreibt jeweils drei Bytes Rohdaten als vier Zeichen um, die aus einem Alphabet mit 64 Zeichen stammen, und ein paar =-Zeichen runden das Ende ab, wenn die Anzahl der Bytes kein Vielfaches von drei ist. Dekodieren ist die schrumpfende Richtung dieses Tauschs: vier Zeichen gehen rein, drei Bytes kommen raus. Die Startseite dieser Site erklärt das Format Schritt für Schritt, also widmet sich dieser Artikel dem, wo es wirklich hingehört: der PHP-Seite der Aufgabe.

Zuerst die wichtigste Nachricht. PHP liefert seit PHP 4 einen Base64-Dekodierer direkt im Kern mit. base64_decode() braucht keine Erweiterung, kein Composer-Paket und keine Konfiguration, und es läuft überall dort, wo PHP läuft. Die weniger gute Nachricht: Die Standardlaune verschluckt defekte Eingaben still und leise und reicht Ihnen Müll, ohne ein Wort dazu zu sagen. Die gute Nachricht wird noch besser: Ein einziges Flag ($strict) macht aus der Funktion einen ordentlichen Torwächter, und wenn Sie wissen, wie man die Laune auswählt, belegt, dass die Eingabe echt ist, und die Bytes wieder in Bedeutung übersetzt, hört Base64 auf, eine Quelle mysteriöser Bugs zu sein, und wird zu einer Routine, die Sie automatisieren können.

Ein kurzer Größen-Hinweis: Dekodieren macht Daten um etwa ein Viertel kleiner (drei Bytes raus für vier Zeichen rein), daher belegt das Ergebnis immer weniger Speicher als die Eingabe. Sie müssen sich nie Sorgen machen, dass ein Dekodieren explodiert. Jetzt lernen wir das Werkzeug kennen.

Die Funktion, die die Arbeit macht

Hier ist die komplette Signatur, genau so, wie sie modernes PHP meldet:

base64_decode(string $string, bool $strict = false): string|false

Drei Wörter in dieser Zeile machen die ganze Arbeit. $string hat kein Größenlimit: Ein Megabyte ist in gut einer Millisekunde dekodiert, also steht nichts im Wege, eine komplette Datei in einem einzigen Aufruf zu dekodieren. Der Rückgabe-Typ benennt den ganzen Vertrag: entweder ein String mit dekodierten Bytes oder false. Keine Ausnahmen, keine Fehlercodes, kein zweiter Kanal. false ist das einzige Signal, das Sie bekommen, daher gehört die Prüfung dazu zum Handwerk. Und ein Satz aus dem Handbuch verdient es, auswendig gelernt zu werden: die zurückgegebenen Daten können binär sein. Im Moment, in dem das Ergebnis eine PNG, eine ZIP oder einen Hash enthält, ist es in keinem weiten Sinne ein "Textstring", und PHP wird Sie nur allzu gern trotzdem so behandeln lassen. Diese Flexibilität ist eine Superkraft und eine Falle zugleich, und die Abschnitte unten halten sie im Zaum.

Ein schneller Rundgang über die Versionstags, denn übernommener Code nimmt Dinge gern einfach an. Die Funktion steckt seit PHP 4 im Kern. Ihr $strict-Parameter kam in PHP 5.2.0, im November 2006. Seit PHP 8.0 trägt die Signatur echte native Typen (das string und bool oben, dazu der Rückgabe-Typ string|false), sodass IDEs und statische Analyser endlich wissen, dass die Funktion scheitern kann. Seit PHP 8.1 löst das Übergeben von null eine Deprecation-Warnung aus; wenn Sie "nichts" meinen, schreiben Sie '' ausdrücklich:

$decoded = base64_decode('');
var_dump($decoded); // string(0) ""

Strict-Mode oder stille Aufräumaktion

Das $strict-Flag ist ein Schalter zwischen zwei sehr verschiedenen Persönlichkeiten. Aus (die Standardeinstellung) ist der Dekodierer ein freundlicher Vergesser: Jedes Zeichen außerhalb des Base64-Alphabets wird still und leise verworfen, der Rest wird dekodiert, und niemand erfährt etwas davon. Das Handbuch sagt es ungeschminkt: andernfalls werden ungültige Zeichen still und leise verworfen. An ist der Dekodierer ein Torwächter: Das erste Zeichen, das er nicht kennt, bringt dem gesamten Payload ein false ein.

Hier ist der Schadensreport. Jede Zeile unten ist echtes Verhalten von base64_decode() unter PHP 8.x:

Eingabe Ohne Strict (Standard) Strict
Zm9vYmFy, sauber "foobar" "foobar"
Zm9v\r\nYmFy, CRLF mitten im String "foobar" "foobar"
" Zm9vYmFy ", Leerzeichen an beiden Enden "foobar" "foobar"
Zm9v\x0bYmFy, vertikaler Tabulator "foobar" false
Zm9v\x00YmFy, eingebettetes NUL-Byte "foobar" false
V@hpcy, ein verirrtes @ 3 Bytes Müll false
Zm9vY, fünf Zeichen "foo", letztes Zeichen weggelassen false
Z, ein einzelner Buchstabe "", ein leerer String false
=Zm9, Padding ganz vorn "fo" false
Zm9vYmFy==, Pads hinter einer vollen Gruppe "foobar" false
Zm9vYmFy==A, Daten nach den Pads "foobar" false
Zm9vYmF, sieben Zeichen, ohne Pads "fooba" "fooba"

Drei Zeilen verdienen einen zweiten Blick. Die Zeile mit V@hpcy zeigt, warum der Modus ohne Strict überall dort gefährlich ist, wo die Eingabe nicht vertrauenswürdig ist: Das verirrte @ stoppt das Dekodieren nicht; es verschwindet einfach, und die drei Bytes, die herauskommen, bedeuten nichts. Die Zeile mit dem einzelnen Z zeigt, dass ein leeres Ergebnis fast nichts beweist; ein einzelnes Zeichen als Payload "dekodiert" zu einem leeren String, ohne zu scheitern. Die Zeile mit Zm9vYmFy==A zeigt den Dekodierer, der fröhlich Daten ignoriert, die nach dem Padding auftauchen; genau so kann ein abgeschnittenes oder manipuliertes Payload einwandfrei aussehen.

Was lässt der Strict-Mode trotzdem durch? Genau vier Whitespace-Zeichen: Leerzeichen, Tab, Wagenrücklauf und Zeilenvorschub, an jeder Position, sogar direkt neben den =-Zeichen. Das ist Absicht. MIME-umbrachte E-Mail-Payloads tragen CRLF-Zeilenbrüche im kodierten Stream, und der Strict-Mode verschluckt sie ohne Vorverarbeitung (der E-Mail-Abschnitt unten erklärt warum). Alles andere, was kein Alphabetzeichen ist, von NUL-Bytes bis zu vertikalen Tabulatoren, bringt ein false ein.

Es gibt eine echte Nachsicht, die Sie kennen sollten, auch wenn es keine PHP-Eigenheit ist: PHP füllt fehlendes Padding still und leise für Sie nach. Das sieben Zeichen lange Payload Zm9vYmF (gar keine Pads) dekodiert zu "fooba", genauso wie sein Cousin mit Pads, Zm9vYmF=, in beiden Launen. RFC 4648 verlangt im Allgemeinen Padding, daher ist das Akzeptieren eines unpaddierten Endes eine bewusste Lockerung, und sie ist nicht PHP-spezifisch: Das RawStdEncoding von Go und Javas Dekodierer nehmen dieselbe unpaddierte Eingabe. Wenn Ihre PHP-Seite und ein Partner-System bei einem Edge-Case-Payload unterschiedlicher Meinung sind, sollte man zuerst nach einem fehlenden Pad suchen.

Der Standard stimmt mit der strengen Laune überein. RFC 4648, Abschnitt 3.3, sagt: Implementierungen müssen kodierte Daten verwerfen, die Zeichen außerhalb des Alphabets enthalten, sofern die umgebende Spezifikation nichts anderes sagt (MIME ist der klassische "nichts anderes sagt"-Fall). Derselbe Abschnitt erklärt warum: Nicht-Alphabet-Zeichen können als verdeckter Kanal ausgenutzt werden, der Information in Zeichen versteckt, die Ihr Dekodierer wegwirft, und sie wurden schon benutzt, um Dekodierer-Bugs auszulösen. Wenn Ihre Eingabe aus der Außenwelt kommt, ist der Strict-Mode keine Stilfrage. Er ist das, was der Standard verlangt.

Nachweisen, dass ein Payload Base64 ist

Ein Dekodierer, der still und leise scheitern kann, verdient eine Validierungspipeline vor sich her. Drei Schichten, von denen jede fängt, was die anderen übersehen.

Schicht eins ist eine Formprüfung mit einem regulären Ausdruck: nur Alphabetzeichen und höchstens zwei Pads ganz am Ende.

$shapeLooksPlausible = preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $payload) === 1;

Der reguläre Ausdruck fängt offensichtlichen Müll (verirrte Leerzeichen, @-Zeichen, ein Pad mitten im String), bevor irgendetwas anderes läuft. Er ist aber kein Validator: Er sieht nicht, dass Zm9vYmFy= neun Zeichen mit einem Pad sind, was der Strict-Mode ebenfalls ablehnt. Genau dafür existiert Schicht zwei. Das strikte Dekodieren ist der einzige Check, der Base64-Semantik versteht, daher hat es das letzte Wort.

Schicht drei ist die, die jeder vergisst: false ausdrücklich behandeln, denn es ist das einzige Signal, das Sie bekommen.

function decode_payload(string $payload): string
{
  $clean = str_replace(["\r", "\n"], '', $payload);
  $decoded = base64_decode($clean, true);
  if ($decoded === false) {
    throw new InvalidArgumentException('Not a valid Base64 payload.');
  }
  return $decoded;
}

Das str_replace() ganz vorn ist optionaler Komfort: Der Strict-Mode toleriert CRLF ohnehin, aber das Entfernen hält jede Längsberechnung, die Sie später anstellen, sauber, denn die Zeichenzahl eines sauberen Payloads ist immer ein Vielfaches von vier. (Eins mehr als ein Vielfaches von vier, wie fünf oder neun, ist in Base64 unmöglich, und der Strict-Mode lehnt es ab.) Beachten Sie, dass die Funktion nie von allein wirft; den Check schreiben Sie.

URL-sichere Base64

In freier Wildbahn treffen Sie auf ein zweites Alphabet, und es ist das, das beißt. Standard-Base64 verwendet + und /, zwei Zeichen, die in URLs Ärger machen: Ein + in einer Query-String wird zu einem Leerzeichen interpretiert, noch bevor PHP es sieht, und / ist ein Pfad-Trennzeichen. RFC 4648, Abschnitt 5, definiert die Lösung: das URL- und Dateinamen-sichere Alphabet, in dem aus + ein - wird, aus / ein _, und das abschließende =-Padding meist weggelassen wird, um Zeichen zu sparen. Der RFC stellt unmissverständlich klar, dass dies "nicht als dasselbe wie die base64-Kodierung betrachtet werden sollte", und der Name, den Sie am öftesten hören werden, ist base64url. JSON Web Tokens, OAuth-State-Parameter, API-Session-IDs und Videoseiten-URLs leben alle in diesem Dialekt.

Die Dekoder-Seite hat zwei Schritte: Das Alphabet zurücktauschen und dann fehlendes Padding wiederherstellen. Hier ist der Helper, den Sie am Ende überall wieder nutzen werden:

function base64url_decode(string $data): string|false
{
  $standard = strtr($data, '-_', '+/');
  $missing = strlen($standard) % 4;
  if ($missing !== 0) {
    $standard .= str_repeat('=', 4 - $missing);
  }
  return base64_decode($standard, true);
}
var_dump(base64url_decode('aGk_PnRoZXJl')); // string(9) "hi?>there"

Modernes PHP ist hier auf Ihrer Seite: Es füllt fehlendes Padding für Sie nach, also ist die explizite Wiederherstellung doppelt gemoppelt (und sie hält Ihren Code portabel zu älteren PHP-Versionen). Die Gefahrenrichtung ist einseitig. Wenn Sie URL-sicheren Text in den Standard-Dekodierer im Modus ohne Strict füttern, sind die Zeichen - und _ einfach nicht im Standard-Alphabet, also werden sie verworfen. Ihre Ausgabe kommt kürzer raus, als sie sollte, ohne Fehler, ohne Hinweis, nichts. Führen Sie den strtr()-Tausch immer zuerst aus, oder besser: Gehen Sie immer über den Helper.

Eine ehrliche Einschränkung: Enthält ein URL-sicheres Payload zufällig weder - noch _, sind die beiden Alphabete für genau diese Daten byte-identisch, und es spielt keine Rolle, welchen Dekodierer Sie verwendet haben. Die Gefahr taucht nur auf, wenn diese Zeichen vorhanden sind, denn das ist der einzige Ort, an dem sich die Alphabete unterscheiden.

Text, Bytes und Zeichensätze

Base64 hat keine Ahnung, was Ihre Bytes bedeuten, und PHPs Dekodierer erbt diese Blindheit. Der Codec ist zeichensatz-blind: Er gibt dieselben 8-Bit-Werte zurück, die reingegangen sind, ob sie UTF-8-Text, Windows-1252-Text, ein JPEG oder ein Hash sind. PHP selbst ist derselben Meinung: Ein String ist eine Folge von Bytes, nichts weiter. Im Moment, in dem Sie das Ergebnis anzeigen oder mit anderem Text vergleichen wollen, muss jemand zwei Fragen beantworten: Ist das überhaupt Text, und wenn ja, in welchem Zeichensatz?

Der praktische Test hat zwei Eimer. Binärdaten melden sich fast immer mit NUL- und niedrigen Steuerbytes, und Text, der kein gültiges UTF-8 ist, ist der zweite Eimer. Die mbstring-Erweiterung (nicht standardmäßig aktiviert) gibt Ihnen den strengen UTF-8-Check:

function looks_binary(string $bytes): bool
{
  if ($bytes === '') {
    return false;
  }
  if (strpbrk($bytes, "\x00\x01\x02\x03\x04") !== false) {
    return true;
  }
  return !mb_check_encoding($bytes, 'UTF-8');
}
var_dump(looks_binary("\x89PNG\r\n\x1a\n...png body")); // bool(true)
var_dump(looks_binary("héllo wörld, 日本語"));          // bool(false)

Wenn das Payload Text in einem Legacy-Zeichensatz ist, konvertieren Sie es, bevor es Ihr HTML berührt. Windows-1252 ist die häufigste Legacy-Kodierung für Web- und Desktop-Daten, und der Unterschied zwischen ihr und einfachem ISO-8859-1 entscheidet, ob das Byte 0x93 ein geschwungenes Anführungszeichen oder ein unsichtbares Steuerzeichen ist:

// "café" in Windows-1252: das é ist ein einzelnes Byte, 0xE9
$legacy = base64_decode('Y2Fm6Q==', true);
$utf8 = mb_convert_encoding($legacy, 'UTF-8', 'Windows-1252');
var_dump($utf8); // string(5) "café": das é ist jetzt zwei UTF-8-Bytes

Eine Warnung vor dem berühmten mb_detect_encoding(): Das PHP-Handbuch selbst sagt, dass automatische Erkennung "nie vollständig verlässlich sein kann", und vergleicht sie mit dem Entschlüsseln einer Nachricht ohne Schlüssel. Füttern Sie ihm ein Windows-1252-"café", und es kann Windows-1252 sagen; füttern Sie ihm einen PNG-Header, und es kann fröhlich wieder Windows-1252 sagen, denn die ISO-8859-Familie von Zeichensätzen ist für jeden möglichen Byte-Wert definiert und kann daher mit allem übereinstimmen. Behandeln Sie Erkennung als letzten Ausweg, vertrauen Sie einem deklarierten Zeichensatz (ein Header, eine Konfigurationszeile, eine Datenbank-Kollation), wann immer einer existiert, und setzen Sie den Rest auf UTF-8 oder binär.

Wenn das Payload eine Datei ist

Die häufigste Datei-Aufgabe ist das Gegenteil dessen, was irgendeine Export-Routine getan hat: Eine .b64-Textdatei trifft ein, und Sie brauchen die Originaldatei zurück. Mit striktem Dekodieren und einem false-Check ist das schon produktionsreif:

$encoded = file_get_contents('/var/www/uploads/blob.b64');
$decoded = base64_decode($encoded, true);
if ($decoded === false) {
  http_response_code(400);
  exit('That upload is not valid Base64.');
}

PHP-Strings sind nur Bytes, also kümmert sich auf diesem Weg nichts darum, ob das Payload eine Textdatei, ein ZIP-Archiv oder ein Video ist. Die Größenrechnung spielt Ihnen in die Karten: Die dekodierte Ausgabe ist drei Viertel so lang wie die kodierte Eingabe, also verschlechtert Dekodieren den Speicher nie.

Eine gute Gewohnheit ist es, die Bytes sich selbst vorstellen zu lassen, bevor Sie irgendein Label glauben. Die finfo-Klasse (die fileinfo-Erweiterung, in Standard-PHP-Builds enthalten) sagt Ihnen, was die Daten tatsächlich sind:

$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
var_dump($mime); // string(9) "image/png"
$extensions = ['image/png' => 'png', 'application/pdf' => 'pdf', 'application/zip' => 'zip'];
$ext = $extensions[$mime] ?? 'bin';
$target = '/var/www/uploads/file-' . bin2hex(random_bytes(4)) . '.' . $ext;
file_put_contents($target, $decoded);

Der letzte Schritt ist wichtiger, als er aussieht. Ein Payload, das vorgibt, ein Bild zu sein, aber zu etwas anderem dekodiert, ist genau die Art von Sache, die eine zweite Meinung fängt. Und wenn Sie die wiederhergestellte Datei später an einen Browser liefern, sollte das Content-Type, das Sie senden, aus demselben finfo-Check kommen, nicht aus dem Dateinamen.

Data-URIs, das Zwischenablage-Format

Ein Klassiker unter den Ankömmlingen: Jemand fügt ein Bild in ein Formular ein, und das Frontend reicht Ihnen eine komplette Data-URI: data:image/png;base64,iVBORw0KGgo.... RFC 2397 definiert die Form: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma und dann die Daten. Wenn das Flag vorhanden ist, ist das Payload Base64; wenn es fehlt, ist das Payload percent-kodierter Klartext, seltener, aber legal. Wird der Medientyp weggelassen, ist die Standardeinstellung text/plain;charset=US-ASCII. Warum hier überhaupt Base64? Weil eine URI keine rohen Bytes oder Kommas sicher enthalten kann, und Base64 Ihnen ein Alphabet gibt, das kein Escaping braucht.

function split_data_uri(string $uri): ?array
{
  if (!str_starts_with($uri, 'data:') || !str_contains($uri, ',')) {
    return null;
  }
  $meta = substr($uri, 5, strpos($uri, ',') - 5);
  $payload = substr($uri, strpos($uri, ',') + 1);
  $isBase64 = str_ends_with($meta, ';base64');
  $mime = $isBase64 ? substr($meta, 0, -7) : $meta;
  if ($mime === '') {
    $mime = 'text/plain;charset=US-ASCII';
  }
  return [$mime, $isBase64, $payload];
}
$uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ';
[$mime, $isBase64, $payload] = split_data_uri($uri);
var_dump($mime); // string(9) "image/png"

Zwei Fehlerquellen leben in diesem Format. Die erste ist das fehlende ;base64-Flag: Eine legale Data-URI ohne das Flag trägt ein percent-kodiertes Payload, und wenn man es durch base64_decode() schickt, entsteht Müll. Die zweite ist der behauptete Medientyp: Er ist ein Hinweis vom Absender, keine Tatsache. Der finfo-Check aus dem Datei-Abschnitt ist Ihre Tatsache. Und denken Sie an den eigenen Rat des RFCs, dass Data-URIs nur für kurze Werte nützlich sind; ein megabytegroßes Bild in einer URL ist ein Stinker, kein Muster.

JWTs: Tokens, in die man reinschauen kann

Das berühmteste Base64-Payload im Web ist der JSON Web Token, und der am wenigsten beängstigende, sobald man die Form kennt. Laut RFC 7519 besteht ein kompakter JWT aus drei URL-sicheren Base64-Teilen, getrennt durch Punkte: ein Header, ein Payload und eine Signatur, jeweils ohne Padding und ohne Zeilenbrüche kodiert (RFC 7515 stellt ausdrücklich klar, dass keine zusätzlichen Zeichen einschleichen dürfen). Der Header und das Payload sind einfaches JSON, daher kann jeder sie lesen, und deshalb sollte jeder den nächsten Absatz verstehen, bevor er einen Token anfasst.

Das Lesen der ersten beiden Teile ist mit dem Helper von oben Arbeit für fünf Zeilen, und es ist ein guter Weg, einen Token zu entmystifizieren:

$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8GljXWCrvkTYln_WtTVhyWSzflOC1iGL8jBDUHQmaEE';
[$headerPart, $payloadPart] = explode('.', $token);
$header  = json_decode(base64url_decode($headerPart), true);
$payload = json_decode(base64url_decode($payloadPart), true);
var_dump($header);
// array(2) { ["alg"] => string(5) "HS256" ["typ"] => string(3) "JWT" }
var_dump($payload);
// array(3) { ["sub"] => string(10) "1234567890" ["name"] => string(8) "John Doe" ["iat"] => int(1516239022) }

Jetzt der Teil, der zählt: Der dritte Teil ist eine Signatur, und die beiden Teile, die Sie gerade dekodiert haben, sind weder geheim noch authentifiziert. Jeder mit einer Packet-Capture kann sie lesen, und jeder mit einem Texteditor kann sie umschreiben. Dem Payload zu vertrauen, bevor die Signatur verifiziert wurde, ist der klassische JWT-Bug. In der Produktion sollten Sie diesen Check nicht selbst basteln. Die Antwort aus der Community ist das firebase/php-jwt-Paket, derzeit in v7, konform mit RFC 7519 und verlangt PHP 8.0 oder neuer. Installieren Sie es mit Composer:

composer require firebase/php-jwt

Dann verifiziert die API zuerst und reicht Ihnen das Payload nur, wenn die Signatur stimmt:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = 'correct-horse-battery-staple-long-enough-secret';
try {
  $claims = JWT::decode($token, new Key($secret, 'HS256'));
  var_dump($claims->sub); // eine Eigenschaft, und erst nachdem die Signatur gestimmt hat
} catch (UnexpectedValueException $e) {
  // kaputter Token, schlechte Signatur oder abgelaufene Claims
}

Ein Versionshinweis: v7 der Bibliothek durchsetzt minimale Schlüssel-Längen für die HMAC-Algorithmen, daher wird ein HS256-Secret, das kürzer als 32 Bytes ist, mit einer DomainException abgewiesen, bevor die Signatur verifiziert wird. Halten Sie Ihre Secrets lang; die Bibliothek lässt Sie das nicht vergessen.

Achten Sie auf die Reihenfolge in dieser API: JWT::decode() wirft bei einer schlechten Signatur, einem abgelaufenen Token oder einem fehlenden Algorithmus, statt Müll zurückzugeben, daher ist ein Payload, das Sie zurückbekommen, eines, dem Sie vertrauen können. Die selbst gebastelte Version oben ist zum Verstehen und zum Reinschauen in Tokens, die nicht für Sie gedacht waren; die Bibliothek ist zum Vertrauen.

HTTP Basic Auth, der älteste Header

Der älteste Authentifizierungs-Header im Web fährt immer noch auf Base64. Laut RFC 7617 sendet eine HTTP-Basic-Anfrage Authorization: Basic gefolgt von der Base64-Kodierung von username:password. Der RFC stellt ausdrücklich klar, dass das Kodierung ist, kein Schutz: Jeder mit einer Packet-Capture kann beide Hälften mit einer einzigen Taste dekodieren. Ihre Aufgabe auf der Dekoder-Seite ist es, den Header zu parsen, strikt zu dekodieren und mit einer timing-sicheren Funktion zu vergleichen.

function basic_credentials(string $header): ?array
{
  if (!str_starts_with($header, 'Basic ')) {
    return null;
  }
  $decoded = base64_decode(substr($header, 6), true);
  if ($decoded === false || !str_contains($decoded, ':')) {
    return null;
  }
  [$user, $password] = explode(':', $decoded, 2);
  return [$user, $password];
}
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$creds = basic_credentials($header);
if ($creds !== null
  && hash_equals('alice', $creds[0])
  && hash_equals('secret123', $creds[1])
) {
  // authentifiziert
}

Zwei Details halten das sicher. Das Limit von 2 in explode() ist wichtig, denn ein Passwort darf legal Doppelpunkte enthalten, und der Vergleich sollte hash_equals() sein, niemals ==, damit ein Angreifer sich nicht über Timing durch Ihre Benutzerliste hangeln kann. Und servieren Sie das nur über HTTPS; auf einer unverschlüsselten Verbindung ist die Base64-Schicht nur hübsche Verpackung.

E-Mail, wo alles begann

Base64 wurde für ein bestimmtes Problem geboren: Der Mail-Transport trug nur 7-Bit-ASCII, und trotzdem wollten die Leute Binärdaten senden. Der MIME-Standard (RFC 2045, Abschnitt 6.8) machte Base64 zu einer der binären Transfer-Kodierungen und fügte zwei Hausregeln hinzu. Erstens dürfen kodierte Zeilen 76 Zeichen nicht überschreiten. Zweitens muss die Dekodier-Software jedes Zeichen außerhalb des Alphabets ignorieren, Zeilenbrüche inklusive. Genau diese zweite Regel ist der Grund, warum PHPs Dekodierer in beiden Launen ein CRLF-umbrachtes Payload verschluckt, ohne jegliche Vorverarbeitung von Ihnen. (Das ist auch der Ursprung der \r\n-Toleranz, die Sie in der Strict-Mode-Tabelle oben gesehen haben.)

$png = "\x89PNG\r\n\x1a\n" . random_bytes(256);
$wrapped = chunk_split(base64_encode($png), 76, "\r\n");
// später, auf der empfangenden Seite, kein Aufräumen nötig:
$decoded = base64_decode($wrapped, true);
var_dump($decoded === $png); // bool(true): jedes Byte war auf der Rundreise intakt

Zwei praktische Hinweise. Erstens bringt das Umbruch-Gewicht: Bei einem CRLF alle 76 Zeichen trifft ein 100-KB-Anhang als rund 137 KB Text an (der übliche 4/3-Faktor, plus der Zeilenbruch-Overhead). Zweitens gibt es für reale Mails mit Headern, mehreren Teilen und quoted-printable-Geschwistern die optionale mailparse-Erweiterung, die komplette RFC-822-Nachrichten Teil für Teil zerlegt; für einen einzigen bekannten Anhang ist striktes Dekodieren alles, was Sie brauchen.

PEM-Rüstung: Schlüssel und Zertifikate

Zertifikate und Schlüssel reisen in PEM-Rüstung: ein BEGIN-Label, ein Block Base64 in 64-Zeichen-Zeilen und ein END-Label. Die Zeilenlänge von 64 Zeichen ist eine Konvention, die aus der ursprünglichen Privacy Enhanced Mail-Spezifikation (RFC 1421) stammt, und OpenSSL-Tools erwarten sie, daher spielt sie eine Rolle, wenn Sie neu rüsten. Beim Dekodieren spielt sie überhaupt keine Rolle: Der Dekodierer ignoriert die Zeilenbrüche einfach.

$pem = file_get_contents('/etc/ssl/my-key.pem');
preg_match('/-----BEGIN ([A-Z ]+)-----\s*(.*?)\s*-----END \1-----/s', $pem, $m);
$label = $m[1];
$der = base64_decode(preg_replace('/\s+/', '', $m[2]), true);
if ($der === false) {
  // doch kein Base64
}
var_dump($label); // string(11) "PRIVATE KEY"

Die dekodierten Bytes sind DER, eine kompakte binäre Serialisierung, und genau damit arbeiten die openssl_*-Funktionen letztlich. Der Rückverweis \1 im regulären Ausdruck ist der stille Held: Er garantiert, dass das END-Label zum BEGIN-Label passt, und so vermeiden Sie, das END eines Zertifikats an das BEGIN eines Schlüssels zu verkleben, wenn eine Datei mehrere Blöcke enthält.

Streams und große Payloads

Dekodieren ist die Richtung, die Ihnen hilft: Die Ausgabe ist drei Viertel so groß wie die Eingabe, daher ist Speicherdruck durch Base64 selten. Trotzdem, wenn eine .b64-Datei mit mehreren hundert Megabytes auf der Festplatte landet, haben Sie zwei Werkzeuge, um den Fußabdruck flach zu halten.

Das erste ist das Dekodieren in Chunks. Teilen Sie die bereinigte Eingabe in Stücke, deren Länge ein Vielfaches von vier Zeichen ist, dekodieren Sie jedes Stück strikt und fügen Sie sie zusammen. Jeder Chunk ist ein eigenständiges gültiges Payload, daher geht nichts an den Grenzen verloren, und eine defekte Datei scheitert früh mit einem Offset, das Sie melden können.

$clean = str_replace(["\r", "\n"], '', file_get_contents('/var/www/uploads/huge.b64'));
$decoded = '';
$chunkSize = 4 * 50000; // ein Vielfaches von vier Zeichen, pro Aufruf etwa 150 KB raus
for ($offset = 0; $offset < strlen($clean); $offset += $chunkSize) {
  $part = base64_decode(substr($clean, $offset, $chunkSize), true);
  if ($part === false) {
    exit('Corrupted payload near offset ' . $offset);
  }
  $decoded .= $part;
}

Ein Megabyte Base64 wird auf moderner Hardware in gut einer Millisekunde dekodiert, daher kostet diese Schleife fast nichts; wählen Sie sie wegen ihrer Validierungs- und Meldungs-Eigenschaften, nicht wegen der Geschwindigkeit.

Das zweite Werkzeug ist ein Bürger der Streaming-Welt: der convert.base64-decode-Stream-Filter. Er funktioniert auf jedem PHP-Stream, sodass Sie direkt aus einem Datei-Zeiger, php://input oder einem Memory-Stream dekodieren können, ohne den ganzen kodierten Text jemals in einer einzigen Variable zu halten. Wie die Funktion ohne Strict überspringt er einfach jedes Zeichen außerhalb des Base64-Alphabets:

$in = fopen('/var/www/uploads/huge.b64', 'rb');
$out = fopen('/var/www/uploads/huge.bin', 'wb');
stream_filter_append($in, 'convert.base64-decode', STREAM_FILTER_READ);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

Welches Werkzeug wählen Sie? Den Filter, wenn die Daten durch einen Stream fließen und Sie PHP die Kleinarbeit überlassen wollen; die Chunk-Schleife, wenn Sie Validierung pro Chunk, Fortschritts-Meldungen oder das Offset der Beschädigung brauchen.

Datenbanken, Konfigurationsdateien und Umgebungsvariablen

Base64 ist ein Textcontainer, deshalb taucht es an Orten auf, die Sie nicht erwarten würden. In Datenbanken kann ein binärer Blob (eine Datei, eine Ikone, eine serialisierte Struktur) als Base64 in einer TEXT-Spalte leben und jedes Tool überleben, das Text annimmt. Erwarten Sie, dass der gespeicherte Wert etwa 33 Prozent größer ist als das Original, und dimensionieren Sie Ihre Spalten entsprechend. In Konfigurationsdateien und Umgebungsvariablen ist Base64 der Trick, um Werte einzuschmuggeln, die sonst das Format kaputtmachen würden: eine Datenbank-DSN mit Semikolons, ein Passwort mit Anführungszeichen, ein Wert mit Zeilenumbruch.

// .env oder Konfiguration, geschrieben von der Ops-Person:
//   DB_DSN_B64 = cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}
// $dsn ist jetzt: pg:host=db;password=qu"ote

Die gleiche Vorsicht gilt hier doppelt. Erstens: Das ist Formatsicherheit, kein Schutz: Im Moment, in dem ein Entwickler die Konfigurationsdatei liest, kann er den Wert in einem einzigen Aufruf dekodieren. Speichern Sie nie ein Secret als Base64 und nennen Sie es verschlüsselt. Zweitens: Validieren Sie beim Start: Ein defekter oder halb eingefügter Umgebungs-Wert ist ein false vom strikten Aufruf, und ein einzeiliger Check verwandelt einen kryptischen Laufzeit-Fehler in eine handhabbare Startmeldung.

Von der Kommandozeile

Nicht jedes Dekodieren findet innerhalb einer Web-Anfrage statt. CLI-Skripte, Cron-Jobs und One-Liner dekodieren Base64 ständig, und die Kommandozeile ist der Ort, an dem die Funktion auf php://stdin trifft:

php -r 'fwrite(STDOUT, base64_decode(file_get_contents("php://stdin"), true));' < payload.b64 > restored.bin

Die Shell hat bereits ihr eigenes Base64-Werkzeug (coreutils base64 -d), und es ist für schnelle Arbeit gut; der PHP-One-Liner ist für den Fall, dass der nächste Schritt PHP-Logik ist: Schreiben in eine Datenbank, Aufruf einer API, Ausführung einer Validierung. Zwei Shell-spezifische Fallstricke. Die Ausgabe eines Dekodierens sind rohe Bytes, also schicken Sie sie in eine Datei oder in ein Kommando, das Bytes versteht, nicht in ein Terminal, das sie verunstalten würde. Und lassen Sie das Strict-Flag im One-Liner an, denn ein abgeschnittenes Einfügen in einem Terminal verdient ein false, nicht drei Bytes Müll.

Fehlerquellen mit PHP-Akzent

Ein kurzer Rundgang durch die Fallen, die PHP-spezifisch sind, an einem Ort gesammelt:

  • Der Standard ohne Strict ist der große. base64_decode('V@hpcy') gibt drei Bytes Müll ohne Warnung zurück, daher braucht jeder Dekodierer nicht vertrauenswürdiger Eingaben das Strict-Flag und einen false-Check.
  • Ein einzelnes Zeichen dekodiert im Modus ohne Strict zu einem leeren String, genau wie ein String aus nur Leerzeichen. Ein leeres Ergebnis beweist fast nichts; nur false bedeutet Scheitern, und das bekommen Sie nur im Strict-Mode.
  • Das + in einer Query-String ist schon ein Leerzeichen, bevor PHP es sieht. Wenn ein Client ?token=abc+def sendet, ohne es zu percent-kodieren, reicht Ihnen PHP abc def (das ist Formular-Kodierungs-Verhalten, geteilt von parse_str() und urldecode()), und keine Menge Dekodier-Magie bringt das Plus zurück. URL-sichere Base64 (überhaupt kein Plus) ist die Lösung für Tokens in URLs.
  • Fehlendes Padding wird still und leise für Sie nachgefüllt. Sieben Zeichen dekodieren wie acht; das ist bequem, aber es bedeutet, dass ein um ein oder zwei Pads abgeschnittenes Payload immer noch ohne Protest dekodiert werden kann, daher beweist ein sauberes Dekodieren nie ganz, dass das Payload heil angekommen ist (Gos Raw-Encoders und Java sind ebenso nachsichtig).
  • Der Geist von mbstring.func_overload. Die lange deprecated Einstellung, die strlen() und Co. umschrieb, um Zeichen zu zählen (in PHP 8.0 entfernt), hat früher die Base64-Byte-Mathematik bei UTF-8-Strings kaputt gemacht. Legacy-Code, den Sie erben, kann immer noch Kommentare und Workarounds dafür tragen. Löschen Sie sie.
  • Dekodierte Bytes sind kein UTF-8-String. preg_match() mit dem /u-Flag oder mb_substr() auf dekodiertem Binär laufen zu lassen, ist eine sofortige Quelle für "ungültige Eingabe"-Fehler. Erst schnüffeln, dann entscheiden.
  • Das Übergeben von null ist seit PHP 8.1 deprecated. Wenn eine Variable null sein kann, coaleszen Sie sie vor dem Aufruf zu ''.
  • $_GET und Co. werden mit Formular-Regeln dekodiert, nicht mit URL-Regeln. Wenn ein Wert percent-kodiert ankam, ist rawurldecode() die sicherere Umkehrung, denn es lässt + in Ruhe.

Eine kurze Geschichte von base64_decode

Base64 selbst ist älter als der Großteil des modernen Webs (der dafür zuständige Standard, RFC 4648, stammt aus dem Jahr 2006, und er schrieb die MIME-Kodierung von 1996 fest, die selbst aus der PEM-Rüstung der frühen 1990er-Jahre stammt). Die PHP-Geschichte ist ihr eigener kleiner Changelog.

PHP 4 brachte base64_decode() als Kernfunktion ohne Optionen und ohne Strict-Mode; die nachsichtige Laune ohne Strict war die einzige Laune, und es gab keinen Weg, den Dekodierer zu bitten, sich zu beschweren. PHP 5.2.0, im November 2006, fügte das $strict-Flag hinzu, und der Changelog-Eintrag ist lesenswert: Es wurde hinzugefügt, um die Konformität mit RFC 3548 durchzusetzen, dem Vorgänger des heutigen RFC 4648. Dieses eine Flag erwies sich als der nützlichste Zuwachs in dem Leben der Funktion.

Dann kamen die Debugging-Jahre. PHP 5.3 behebte eine Reihe von Strict-Mode-Bugs über zwei Punkt-Releases: Bug #52327 (leading Padding wurde im Strict-Mode unrichtig behandelt, behoben in 5.3.4) und Bug #55273 (Whitespace nach dem Padding wurde im Strict-Mode abgelehnt, behoben in 5.3.9). (Eine Integer-Overflow-Behebung aus dem Jahr 2016 ist ebenfalls unter dem Namen dieser Funktion registriert: Bug #72836, offiziell mit dem Titel "integer overflow in base64_decode caused heap corruption" und behoben in 5.6.25, aber der Reproduktionscode und die gepatchte Funktion des Bug-Reports selbst zeigen, dass der eigentliche Overflow in der Längsberechnung von base64_encode() lag, nicht im Dekodierer; der Titel ist eine Fehlbenennung, die vom ursprünglichen Report geerbt wurde.) Jede Behebung strammte das Verhalten an, das Sie in der Tabelle oben sehen. PHP 8.0 gab beiden Base64-Funktionen native Parameter- und Rückgabe-Typen, die Signatur, die Sie oben in diesem Artikel gesehen haben, und dieselbe Release-Reihe entfernte mbstring.func_overload, die Einstellung, die jahrelang still und leise die Byte-Mathematik kaputt gemacht hatte. PHP 8.1 erklärte das Übergeben von null an sie für deprecated. Seither ist die Oberfläche eingefroren: ein Parameter, ein Flag, ein Rückgabe-Typ, unverändert.

Ein paar Nerd-Freuden

Weil dies eine Longform-Referenz ist, hier einige PHP-spezifische Fakten, die einfach Spaß machen:

  • Die leere Identität. base64_encode('') und base64_decode('') sind beide ''. Die Funktionen behandeln Leere in beiden Richtungen als erstklassigen Wert, ohne dass false beteiligt ist.
  • Eine seltsame Adresse. Im PHP-Handbuch leben beide Base64-Funktionen im Kapitel "URLs" des Buchs "Other Basic Extensions". Es gibt kein eigenes "Encoding"-Kapitel; dort finden Sie sie, oben in der Liste dieses Kapitels, vor parse_url() und seinen Freunden.
  • Der Dekodierer ist ein Homomorphismus. Eine klassische php.net-Nutzeranmerkung beobachtet, dass die Funktion ein Homomorphismus zwischen modulo-4- und modulo-3-segmentierten Strings ist, was die formale Art zu sagen ist, dass jede Aufteilung in ein Vielfaches von vier eine gültige Aufteilung ist. Deshalb funktioniert der Chunk-Dekodierungs-Abschnitt überhaupt, und deshalb kann eine 1-MB-Datei in 50-KB-Scheiben ohne jeden Verlust dekodiert werden.
  • Ein Parameter, ein Flag. In mehr als zwanzig Jahren bekam base64_decode() genau einen Parameter ($strict), und base64_encode() bekam keinen.
  • Es hat ältere Geschwister. Dieselbe Kern-Erweiterung trägt auch convert_uuencode() und convert_uudecode() (im Handbuch unter String Functions aufgelistet), die Relikte der Dial-up-Ära, als uuencode der Binär-Transport der Wahl war. Sie werden sie fast nie brauchen, aber wenn je eine uralte .uu-Datei in Ihrem Posteingang landet, kann PHP sie öffnen.
  • Der Strict-Mode hält eine offene Tür für E-Mail. Die vier Whitespace-Zeichen (Leerzeichen, Tab, Wagenrücklauf und Zeilenvorschub) fahren absichtlich glatt durch den Strict-Mode, sodass ein MIME-umbrachter Anhang keine Vorverarbeitung braucht. Alles andere, NUL-Bytes inklusive, ist ein false.

Die andere Richtung

Das ist die Dekoder-Seite, und hier lebt der größte Teil des Schmerzes, denn beim Dekodieren treffen Sie auf die Daten anderer: ihre Padding-Entscheidungen, ihre Zeilenbrüche, ihre Zeichensätze, ihre Tokens. Die andere Richtung, Bytes in einen Base64-String umzuwandeln mit base64_encode(), ist ein ruhigeres Tier: Es scheitert nie, es hat keinen Strict-Mode, und sein eigener Satz an Fallen (Doppelkodierung, Umbruch-Mismatch, die Größen-Rechnung) bekommt seinen eigenen Guide. Base64-Kodierung in PHP, verlinkt von dieser Seite, behandelt den Encoder in derselben Tiefe.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Kodierung in PHP: Ein vollständiger Leitfaden