JSON, serialize() und die Grenze dazwischen
PHP-Serialisierung entscheidet über mehr als nur das Datenformat: serialize() ist schnell und erhält jede Objekteigenschaft, wird bei fremden Daten aber schnell zum Einfallstor für Object Injection. JSON ist sicher und sprachunabhängig, kostet dafür Objektfidelity und etwas Geschwindigkeit. Dieser Beitrag zeigt, wie beide Formate intern funktionieren, wo unserialize() gefährlich wird und wie eine saubere Migration in bestehenden Systemen gelingt.
Inhaltsverzeichnis
- 1. Was PHP-Serialisierung bedeutet und wofür sie gebraucht wird
- 2. Native Serialisierung mit serialize() und unserialize()
- 3. JSON-Serialisierung mit json_encode() und json_decode()
- 4. Sicherheitsrisiken von unserialize(): Object Injection und POP-Chains
- 5. unserialize() härten: allowed_classes und sichere Alternativen
- 6. Performance-Vergleich: Geschwindigkeit, Speicherbedarf und Payload-Größe
- 7. Datentypen-Fallstricke: Objekte, Properties und JsonSerializable
- 8. Wann native Serialisierung noch sinnvoll ist, und wann JSON Pflicht ist
- 9. Migration von serialize() zu JSON in bestehenden Systemen
- 10. Zusammenfassung
- 11. FAQ
1. Was PHP-Serialisierung bedeutet und wofür sie gebraucht wird
PHP-Serialisierung bezeichnet die Umwandlung eines Werts im Arbeitsspeicher, eines Skalars, eines Arrays oder eines Objekts, in eine lineare Zeichenkette, die gespeichert, übertragen oder zwischengespeichert und später wieder in denselben Wert zurückverwandelt werden kann. Der Begriff ist bewusst eine Sammelbezeichnung für zwei dominierende Ansätze im PHP-Ökosystem: die native Funktion serialize()/unserialize(), die fest in der Sprache verankert ist, und die JSON-Serialisierung über json_encode()/json_decode(). Beide lösen dasselbe Grundproblem, strukturierte Daten müssen eine Prozessgrenze überschreiten, ohne dabei ihre Form zu verlieren.
Die praktischen Einsatzgebiete sind vielfältig. Das PHP-Session-Handling speichert $_SESSION standardmäßig als serialisierte Zeichenkette im gewählten Session-Handler, egal ob Dateisystem, Redis oder Datenbank. Caching-Schichten wie APCu, Redis oder Memcached speichern berechnete Ergebnisse, Datenbankabfragen, gerenderte Fragmente, Konfigurationsarrays, als serialisierte Strings, weil das Cache-Backend selbst nur Byte-Strings versteht und keine native PHP-Struktur. Message Queues wie RabbitMQ oder SQS brauchen ebenfalls ein Übertragungsformat: Ein Producer serialisiert die Job-Nutzlast, ein Consumer deserialisiert sie auf der anderen Seite, oft in einer anderen Codebasis oder sogar einer anderen Sprache.
APIs setzen heute fast ausschließlich auf JSON, weil HTTP-Clients über Sprachgrenzen hinweg Antwortkörper parsen müssen und JSON sich als gemeinsame Sprache des Datenaustauschs durchgesetzt hat. Genau diese Vielfalt an Einsatzgebieten macht die Wahl der richtigen PHP-Serialisierung-Strategie entscheidend: Eine falsche Wahl im API-Kontext, etwa natives serialize() in einer öffentlichen Schnittstelle, schafft Sicherheits- und Interoperabilitätsprobleme, während eine falsche Wahl in einem rein internen Cache, etwa JSON für tief verschachtelte Objektgraphen, unnötig Performance kostet.
2. Native Serialisierung mit serialize() und unserialize()
serialize() durchläuft den übergebenen Wert rekursiv und erzeugt eine kompakte, selbstbeschreibende Zeichenkette in einem proprietären Format, das nur PHP nativ versteht. Jeder Skalartyp bekommt einen einzelnen Buchstaben als Präfix: b für Boolean, i für Integer, d für Double beziehungsweise Float, s für String, N für Null, gefolgt von der Länge oder dem Wert und einem abschließenden Semikolon. Der String "hello" wird beispielsweise als s:5:"hello"; kodiert, s steht für den Typ, 5 für die Byte-Länge, der zitierte Inhalt für den eigentlichen Wert. Diese Längenangabe ist das auffälligste Merkmal des Formats und der Grund, warum unserialize() so schnell ist, es muss nie nach einem Terminatorzeichen suchen, sondern liest exakt die angekündigte Anzahl Bytes.
Arrays werden als a:N:{...} dargestellt, wobei N die Anzahl der Elemente angibt und die geschweiften Klammern N Schlüssel-Wert-Paare enthalten, abwechselnd Schlüssel und Wert, jeweils einzeln typpräfixiert. Objekte nutzen das Format O:len:"Klassenname":N:{...}, der Klassenname wird also direkt in die Nutzlast eingebettet, gefolgt von den N Eigenschaften, wobei private und protected Eigenschaftsnamen intern maskiert werden, private Eigenschaften bekommen den Klassennamen und Null-Bytes vorangestellt, protected Eigenschaften ein einzelnes Null-Byte-Präfix, damit unserialize() die Sichtbarkeit später korrekt wiederherstellen kann.
Genau diese selbstbeschreibende Eigenschaft, Klassenname und Eigenschaftsstruktur werden direkt im String kodiert, ist innerhalb derselben Codebasis mächtig, aber auch der Grund, warum PHP-Serialisierung mit dem nativen Format gefährlich wird, sobald die Zeichenkette von außen kommen kann: unserialize() versucht bereitwillig, jede im Payload genannte Klasse zu instanziieren, sofern sie autoloadbar ist, noch bevor der eigene Anwendungscode überhaupt die Chance hat, irgendetwas zu validieren.
3. JSON-Serialisierung mit json_encode() und json_decode()
json_encode() wandelt einen PHP-Wert in eine JSON-Zeichenkette nach der sprachunabhängigen JSON-Spezifikation (RFC 8259) um: Objekte werden zu Schlüssel-Wert-Strukturen in geschweiften Klammern, Arrays zu geordneten Listen in eckigen Klammern, Strings sind UTF-8-kodiert mit definierten Escape-Regeln, und es gibt keine native Unterscheidung zwischen Integer und Float jenseits der Zahlendarstellung selbst. Weil JSON keine Klassennamen und keine expliziten Typpräfixe für Skalare trägt, liefert json_decode() standardmäßig stdClass-Objekte für JSON-Objekte, außer der zweite Parameter $associative wird auf true gesetzt, dann liefert die Funktion stattdessen verschachtelte assoziative Arrays, was in modernen PHP-Serialisierung-Workflows mit JSON als Austauschformat die deutlich verbreitetere Wahl ist.
Eine Handvoll Flags verändert das Verhalten von json_encode()/json_decode() in PHP 8.4 spürbar und sollten eher als Standard denn als optionale Extras behandelt werden. JSON_THROW_ON_ERROR wandelt Encoding- oder Decoding-Fehler in eine JsonException um, statt auf das klassische Muster aus stillem false-Rückgabewert plus json_last_error() zu setzen, das im Code-Review leicht übersehen wird. JSON_UNESCAPED_UNICODE hält Nicht-ASCII-Zeichen wie Umlaute oder Emoji als reine UTF-8-Bytes statt als \uXXXX-Escape-Sequenzen, was für Payload-Größe und Log-Lesbarkeit relevant ist. JSON_UNESCAPED_SLASHES vermeidet das Escapen jedes Schrägstrichs in URLs. JSON_PRETTY_PRINT ist nützlich für Debugging-Ausgaben und API-Dokumentationsbeispiele, sollte aber nie für Produktionspayloads verwendet werden, weil es die Größe durch zusätzliche Leerzeichen vervielfacht.
Weil JSON keine eingebaute Objektidentität und keine Klassen-Metadaten kennt, braucht ein vollständiger Round-Trip eines PHP-Objekts durch json_encode()/json_decode(), bei dem am Ende wieder eine Instanz derselben Klasse herauskommt, einen expliziten Vertrag: entweder das JsonSerializable-Interface, das die Encode-Richtung steuert, oder eine Factory beziehungsweise ein Hydrator, der ein dekodiertes Array wieder auf ein typisiertes Objekt abbildet. Das ist eine bewusste Einschränkung und kein Mangel, sie ist genau das, was JSON-basierte PHP-Serialisierung gegen Object Injection absichert, wie der nächste Abschnitt zeigt.
4. Sicherheitsrisiken von unserialize(): Object Injection und POP-Chains
Das zentrale Risiko von unserialize() ist, dass die Funktion mehr tut als Daten zu parsen, sie kann als Nebeneffekt des Parsens von nicht vertrauenswürdigen Eingaben Code zur Ausführung bringen. Sobald das Payload ein O:len:"Klassenname":...-Segment für eine Klasse enthält, die eine von PHPs magischen Methoden implementiert, __wakeup(), __destruct(), __toString() oder in modernem PHP __unserialize(), ruft unserialize() diese Methode automatisch auf, ohne dass der Anwendungscode das Objekt explizit instanziiert hätte. Kontrolliert ein Angreifer die serialisierte Zeichenkette, etwa weil sie aus einem Cookie, einer hochgeladenen Datei oder einem API-Parameter akzeptiert wurde, kontrolliert er damit faktisch mit, welche Klasse instanziiert wird und mit welchen Anfangswerten ihre Eigenschaften belegt sind.
Ausnutzbar wird das durch das, was Sicherheitsforscher eine Property-Oriented-Programming-Kette, kurz POP-Chain, nennen: Der Angreifer muss keinen neuen Code einschleusen, er braucht lediglich Klassen, die irgendwo in der Anwendung bereits geladen sind, eine Framework-Klasse, eine Logging-Klasse, ein Cache-Adapter, deren magische Methoden in Kombination mit angreiferkontrollierten Eigenschaftswerten eine gefährliche Operation ausführen, eine Datei löschen, in einen Pfad schreiben, eine Methode dynamisch aufrufen. Eine einzelne Klasse ist selten für sich genommen gefährlich, das Risiko entsteht durch eine Kette, der Destruktor eines Objekts ruft eine Methode auf einem verschachtelten Objekt auf, dessen eigene magische Methode wiederum etwas anderes tut, bis eine sensible Operation erreicht ist.
Das folgende, bewusst abstrakte Beispiel zeigt den Mechanismus im Kleinen: eine Klasse mit einer __wakeup()-Methode, die abhängig von einem Eigenschaftswert einen Seiteneffekt ausführt. Es zielt gezielt auf kein konkretes reales CVE ab, der Punkt ist zu zeigen, dass der automatische Aufruf von __wakeup() durch unserialize(), rein als Konsequenz des Parsens, die fundamentale Gefahr hinter PHP Object Injection ist, unabhängig davon, welche konkrete Gadget-Kette ein realer Angriff zusammensetzen würde.
<?php
declare(strict_types=1);
// Illustrative example only: shows HOW unserialize() can trigger
// unwanted side effects, not a working exploit for any real CVE.
final class CacheFileHandle
{
public string $path = '';
public string $payload = '';
// __wakeup() runs automatically whenever an instance of this
// class is reconstructed by unserialize(), even if the
// application never intended to create one explicitly.
public function __wakeup(): void
{
// A "harmless-looking" convenience method: writes the payload
// to the path stored in the object's own property.
// If $path and $payload originate from attacker-controlled
// serialized input, the attacker effectively chooses
// WHERE something gets written and WHAT gets written.
file_put_contents($this->path, $this->payload);
}
}
// Application code never calls "new CacheFileHandle()" here at all.
// The dangerous call happens purely as a side effect of parsing:
$untrustedInput = $_COOKIE['cached_state'] ?? '';
// DANGEROUS: unserialize() without allowed_classes will instantiate
// ANY class named in $untrustedInput and call its magic methods.
$restored = unserialize($untrustedInput);
5. unserialize() härten: allowed_classes und sichere Alternativen
Die wirksamste Einzelmaßnahme ist die allowed_classes-Option von unserialize(), die genau eingeführt wurde, um die Angriffsfläche für Object Injection zu schließen. Übergibt man ein explizites Array erlaubter Klassennamen, oder false, um überhaupt keine Objekte zuzulassen, weist das die Engine an, jedes O:...-Segment mit einer Klasse außerhalb dieser Allowlist statt einer echten Instanziierung und einem Aufwachen in eine __PHP_Incomplete_Class-Instanz umzuwandeln. Diese einzige Option verwandelt ein unbegrenztes Problem beliebiger Fernklassen-Instanziierung in eine kleine, überprüfbare Allowlist.
Eine zweite Härtungsebene ist architektonisch: unserialize() auf nicht vertrauenswürdigen Eingaben grundsätzlich vermeiden. Müssen Daten tatsächlich eine Vertrauensgrenze überschreiten, Cookies, hochgeladene Dateien, Antworten von Drittanbieter-APIs, standardisiert man für diese Grenze auf json_decode() und reserviert natives serialize()/unserialize() ausschließlich für Daten, die der eigene Prozess selbst geschrieben hat und intern wieder einliest, etwa einen APCu-Cache-Eintrag. Diese Trennung, natives Format für vertrauenswürdige interne Round-Trips, JSON für alles, was eine Vertrauensgrenze überschreitet, entfernt eine ganze Klasse von PHP-Serialisierung-bezogenen Schwachstellen von vornherein, statt sie durch sorgfältige Konfiguration bei jedem einzelnen Aufruf zu vermeiden.
<?php
declare(strict_types=1);
// Hardened unserialize(): only these two classes may be instantiated,
// everything else becomes __PHP_Incomplete_Class instead of a live object.
$data = unserialize($untrustedInput, [
'allowed_classes' => [CacheEntry::class, CacheMetadata::class],
]);
// Even stricter: allow no objects at all, only scalars and arrays.
$scalarOnly = unserialize($untrustedInput, ['allowed_classes' => false]);
// Safe-alternative pattern: never unserialize() untrusted input at all.
// Decode as JSON and hydrate explicitly through a typed factory instead.
function hydrateCacheEntry(string $json): CacheEntry
{
/** @var array{key: string, value: string, ttl: int} $decoded */
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return new CacheEntry(
key: $decoded['key'],
value: $decoded['value'],
ttl: $decoded['ttl'],
);
}
6. Performance-Vergleich: Geschwindigkeit, Speicherbedarf und Payload-Größe
Bei diesem Thema zählen Messwerte mehr als Intuition. Für einfache skalare Arrays, Strings, Integer, kleine verschachtelte Strukturen, ist serialize()/unserialize() in der Regel schneller als json_encode()/json_decode(), weil die längenpräfixierte Kodierung des nativen Formats das String-Scannen und die UTF-8-Validierung vermeidet, die das JSON-Parsing benötigt. Der Unterschied schrumpft und kann sich bei tief verschachtelten Objektgraphen sogar umkehren, dort ist json_encode() in Kombination mit JsonSerializable häufig konkurrenzfähig, weil sich der Pro-Eigenschaft-Overhead des nativen Formats, die maskierten Eigenschaftsnamen-Marker, aufsummiert.
Bei der Payload-Größe zeigt sich ein ähnliches Bild aus der entgegengesetzten Richtung. JSON ist bei einfachen Arrays und Skalaren meist kompakter, weil es keinen Klassennamen und kein explizites Typ-Zeichen für jeden Wert trägt, natives serialize()-Output für Objekte kann durch eingebettete Klassennamen und maskierte Eigenschaftsnamen-Präfixe, die pro Eigenschaft wiederholt werden, spürbar größer ausfallen. Bei einem Cache-Eintrag, der millionenfach gespeichert wird, summiert sich dieser Größenunterschied zu relevantem Speicherdruck in Redis oder Memcached.
Das folgende Benchmark-Skript veranschaulicht den Messansatz: Beide Round-Trips in hrtime()-Aufrufe einpacken, genügend Iterationen laufen lassen, um Rauschen zu glätten, und Timing sowie strlen() des resultierenden Payloads direkt nebeneinander vergleichen. Jede reale Entscheidung für ein konkretes System sollte auf dieser Art wiederholter, umgebungsspezifischer Messung beruhen statt auf generischen Benchmark-Zahlen aus einem Blogbeitrag, weil die konkrete Datenform mehr Einfluss auf das Ergebnis hat als die Wahl des Serializers allein.
<?php
declare(strict_types=1);
// Simple benchmark comparing native and JSON PHP serialization.
$data = [
'id' => 48213,
'name' => 'Sample Product',
'tags' => ['php', 'serialization', 'benchmark'],
'active' => true,
'price' => 19.99,
];
$iterations = 100_000;
// --- Native serialize()/unserialize() ---
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$native = serialize($data);
$restored = unserialize($native);
}
$nativeTimeMs = (hrtime(true) - $start) / 1_000_000;
// --- JSON encode()/decode() ---
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$json = json_encode($data, JSON_THROW_ON_ERROR);
$restored = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
}
$jsonTimeMs = (hrtime(true) - $start) / 1_000_000;
printf("native: %.2f ms, %d bytes\n", $nativeTimeMs, strlen($native));
printf("json: %.2f ms, %d bytes\n", $jsonTimeMs, strlen($json));
7. Datentypen-Fallstricke: Objekte, Properties und JsonSerializable
Objekte sind der Punkt, an dem sich PHP-Serialisierung zwischen den beiden Formaten am deutlichsten unterscheidet. Natives serialize() erhält die vollständige Klassenidentität und jede Eigenschaft, einschließlich privater und protected Eigenschaften, weil es auf Engine-Ebene mit direktem Speicherzugriff arbeitet. json_encode() serialisiert standardmäßig nur public Eigenschaften eines Objekts, private und protected Eigenschaften sind für die Funktion vollständig unsichtbar, außer die Klasse implementiert JsonSerializable und legt sie explizit über jsonSerialize() offen. Das zu übersehen ist eine häufige Fehlerquelle, eine Klasse mit wichtigen Geschäftsdaten in protected Eigenschaften verliert diese Daten stillschweigend, sobald sie ohne Implementierung des Interfaces durch json_encode() geschickt wird.
Die magischen Methoden __sleep() und __wakeup() lassen eine Klasse ihren eigenen nativen Serialisierungs-Lebenszyklus steuern: __sleep() gibt ein Array von Eigenschaftsnamen zurück, die tatsächlich persistiert werden sollen, nützlich, um nicht serialisierbare Ressourcen wie offene Datenbank-Handles oder Datei-Pointer auszuschließen, und __wakeup() läuft nach unserialize(), um Zustand wiederherzustellen, der den Round-Trip nicht überstehen konnte, eine Verbindung wieder öffnen, einen zwischengespeicherten Wert erneut validieren. Modernes PHP bietet zusätzlich __serialize() und __unserialize(), die mit Arrays statt rohen Eigenschaftsnamen arbeiten und sowohl mit nativer Serialisierung als auch mit anderen Engine-Mechanismen expliziter und besser testbar zusammenspielen.
Eine letzte verbreitete Falle ist die Frage stdClass gegen assoziatives Array nach json_decode(). Wird true als zweites Argument übergeben, liefert die Funktion verschachtelte assoziative Arrays, praktisch für schnellen Zugriff, aber ohne jede Objektidentität oder Typinformation. Wird false übergeben oder das Argument weggelassen, liefert die Funktion stdClass-Instanzen, die Property-Zugriffssyntax unterstützen, aber Typsicherheit und IDE-Autovervollständigung erschweren. Der Großteil an PHP-Serialisierung-Code, der in einer typisierten PHP-8.4-Codebasis mit JSON arbeitet, fährt besser damit, zu Arrays zu dekodieren und anschließend explizite, typisierte Value-Objects zu hydrieren, statt stdClass direkt in der Geschäftslogik zu verwenden.
<?php
declare(strict_types=1);
final class Invoice implements JsonSerializable
{
public function __construct(
private readonly string $number,
private readonly float $total,
private readonly string $internalNote, // deliberately excluded from JSON
) {
}
// Controls what json_encode() actually sees; private properties
// are otherwise invisible to json_encode() by default.
public function jsonSerialize(): array
{
return [
'number' => $this->number,
'total' => $this->total,
// internalNote intentionally omitted from the API payload
];
}
// Controls what gets persisted by native serialize().
public function __sleep(): array
{
return ['number', 'total', 'internalNote'];
}
// Runs after unserialize() rebuilds the object from native format.
public function __wakeup(): void
{
// Example: re-validate invariants after restoring from cache.
if ($this->total < 0) {
throw new UnexpectedValueException('Invoice total cannot be negative');
}
}
}
8. Wann native Serialisierung noch sinnvoll ist, und wann JSON Pflicht ist
Natives serialize() verdient sich seinen Platz noch immer in rein internen, prozesseigenen Kontexten, in denen Geschwindigkeit und vollständige Objekttreue, einschließlich privater Eigenschaften, wichtiger sind als Portabilität. Ein reiner PHP-Session-Handler auf Redis-Basis, ein APCu-Cache mit berechneten Konfigurationsobjekten oder eine Queue, bei der Producer und Consumer garantiert dieselbe PHP-Codebasis im selben Deployment sind, sind legitime Anwendungsfälle, in denen native PHP-Serialisierung ein vernünftiger Standard bleibt, vorausgesetzt die Daten überschreiten niemals eine Vertrauensgrenze von außerhalb der Anwendung.
JSON wird nahezu zur Pflicht, sobald Interoperabilität, Sicherheitsexposition gegenüber fremden Daten oder menschliche Lesbarkeit ins Spiel kommen. Jede öffentliche oder interne API, jede Logzeile, die gegrept oder von einem Log-Aggregator eingelesen werden soll, jede Nutzlast, die an ein JavaScript-Frontend übergeben wird, und jede Datenquelle, die denkbar von außerhalb des eigenen PHP-Prozesses stammen könnte, sollte JSON verwenden, sowohl weil andere Sprachen und Tools es nativ lesen können als auch weil json_decode() keines der Objektinstanziierungsrisiken von unserialize() mitbringt.
| Kriterium | serialize() / unserialize() | json_encode() / json_decode() | Empfehlung |
|---|---|---|---|
| Sicherheit bei fremden Daten | Risiko: Object Injection ohne allowed_classes | Sicher: keine automatische Objektinstanziierung | JSON für alle nicht vertrauenswürdigen Daten |
| Objektunterstützung | Vollständig, inklusive private/protected Properties | Nur public Properties, außer JsonSerializable | Natives Format für interne Objektgraphen |
| Interoperabilität | Nur PHP versteht das Format | Sprachunabhängiger Standard (RFC 8259) | JSON für APIs und Cross-Language-Austausch |
| Payload-Größe | Oft größer durch Klassennamen und Property-Mangling | Meist kompakter bei einfachen Strukturen | JSON für große Cache-Volumen |
| Lesbarkeit | Kaum lesbar, PHP-spezifisches Format | Menschenlesbar, in jedem Editor prüfbar | JSON für Debugging und Logs |
| Performance bei einfachen Arrays | Meist schneller, kein UTF-8-Parsing nötig | Meist etwas langsamer durch String-Validierung | Natives Format bei performancekritischen internen Strukturen |
Die Tabelle macht ein durchgängiges Muster sichtbar: Sobald eine Zeichenkette die Anwendung verlassen oder von außerhalb betreten könnte, gewinnt JSON fast immer, unabhängig davon, ob es um Sicherheit, Interoperabilität oder Lesbarkeit geht. Native PHP-Serialisierung bleibt die richtige Wahl nur dort, wo diese Grenze nachweislich nie überschritten wird.
9. Migration von serialize() zu JSON in bestehenden Systemen
Bestehende, gespeicherte serialize()-Payloads, Session-Daten, Cache-Einträge, auf JSON umzustellen ist selten ein einziger atomarer Schnitt, weil das, was das alte Format geschrieben hat, in einem rollierenden Deployment meist noch irgendwo läuft, und bestehende Einträge in der Regel über eine TTL natürlich ablaufen, statt auf einmal umgeschrieben zu werden. Die praktikable Strategie ist eine Übergangsphase mit Dual-Read und Single-Write: Neue Schreibvorgänge gehen immer als JSON hinaus, Lesevorgänge versuchen zunächst json_decode() und fallen auf den gehärteten Legacy-Pfad über unserialize() mit allowed_classes zurück, sobald das Payload nicht als valides JSON geparst werden kann.
Dieser Fallback braucht eine zuverlässige Methode, die beiden Formate vor dem eigentlichen Parsen zu unterscheiden, in fast jedem realen Fall genügt ein Blick auf das erste Zeichen: Ein JSON-Payload beginnt immer mit {, [, ", einer Ziffer, t, f oder n, während ein natives serialize()-Payload immer mit einem der einbuchstabigen Typ-Marker gefolgt von einem Doppelpunkt beginnt, a:, O:, s:, i:, b:, d: oder N;. Kombiniert mit TTL-basiertem Ablauf bei Cache-Einträgen oder einem Hintergrund-Migrationsjob für länger lebende Daten wie gespeicherte Session-Snapshots lässt sich dieses Dual-Read-Fenster sicher schließen, sobald das Monitoring zeigt, dass der Legacy-Codepfad nicht mehr getroffen wird.
Ein kurzer Beobachtungsschritt schließt den Kreis: Den Fallback-Zweig mit einem Zähler oder einer Logzeile instrumentieren, damit das Team sehen kann, wie die Trefferrate des Legacy-Formats über Tage oder Wochen gegen null geht, statt zu raten, wann es sicher ist, den unserialize()-Fallback vollständig zu entfernen. Erst wenn dieser Zähler über einen vollen TTL-Zyklus hinweg bei null steht, sollte der Legacy-Zweig tatsächlich aus dem Code entfernt werden.
#!/usr/bin/env bash
# migrate-cache-format.sh - Batch-migrates legacy serialize() cache entries to JSON.
set -euo pipefail
REDIS_CLI="redis-cli"
PATTERN="cache:entry:*"
# Iterate all matching keys and re-encode legacy payloads as JSON.
for key in $("$REDIS_CLI" --scan --pattern "$PATTERN"); do
raw="$("$REDIS_CLI" GET "$key")"
# Dual-read strategy: try JSON first, fall back to legacy serialize().
if php -r '
$raw = $argv[1];
json_decode($raw);
if (json_last_error() === JSON_ERROR_NONE) {
exit(0); // already JSON, nothing to migrate
}
exit(1);
' "$raw"; then
continue
fi
# Convert legacy payload to JSON via a hardened PHP helper script.
json_payload="$(php migrate-entry.php --format=legacy -- "$raw")"
"$REDIS_CLI" SET "$key" "$json_payload"
echo "[MIGRATED] $key"
done
10. Zusammenfassung
Die zentrale Erkenntnis zur PHP-Serialisierung ist strukturell, nicht kosmetisch: Natives serialize() kodiert Typ, Länge und Klassenname direkt im String und ermöglicht dadurch unserialize(), jede genannte Klasse zu instanziieren und ihre magischen Methoden auszulösen, ein Verhalten, das bei vertrauenswürdigen, selbst geschriebenen Daten unproblematisch, bei fremden Daten aber ein direktes Einfallstor für PHP Object Injection ist. JSON verzichtet bewusst auf diese Objektidentität und ist damit von Natur aus gegen diese Angriffsklasse immun, kostet dafür aber explizite Arbeit bei Objekthydration über JsonSerializable oder eine Factory.
Wer unserialize() niemals ohne allowed_classes auf potenziell fremde Daten anwendet, JSON konsequent für alles verwendet, was eine Vertrauensgrenze überschreitet, und native Serialisierung auf rein interne, prozesseigene Round-Trips beschränkt, schließt die weit überwiegende Mehrheit aller praktisch relevanten Risiken der PHP-Serialisierung. Für bestehende Systeme mit historisch gewachsenen serialize()-Daten ist eine Dual-Read-Migrationsphase mit beobachtbarer Fallback-Rate der sicherste Weg, ohne Breaking Change auf JSON umzustellen.
PHP-Serialisierung: JSON vs. native, das Wichtigste auf einen Blick
Sicherheit
unserialize() nie ohne allowed_classes auf fremde Daten anwenden. JSON ist die sichere Standardwahl für alles außerhalb der eigenen Vertrauensgrenze.
Format
serialize() kodiert Typ, Länge und Klassenname direkt im String. JSON ist sprachunabhängig und lesbar, aber ohne native Objektidentität.
Performance
Natives Format oft schneller bei einfachen Arrays, JSON meist kompakter bei der Payload-Größe. Messung schlägt Intuition.
Migration
Dual-Read-Strategie mit JSON als Standard und gehärtetem unserialize()-Fallback erlaubt schrittweisen Umstieg ohne Breaking Change.
11. FAQ: PHP-Serialisierung
1Was ist der Unterschied zwischen serialize() und json_encode()?
2Ist unserialize() gefährlich?
3Was ist PHP Object Injection?
4Wie schützt allowed_classes vor Object Injection?
5Warum verliert json_encode() manche Objekteigenschaften?
6Wann JSON statt nativer Serialisierung verwenden?
7Ist JSON immer schneller als serialize()?
8Was macht JSON_THROW_ON_ERROR?
9Wie migriere ich bestehende serialize()-Daten zu JSON?
10__sleep()/__wakeup() vs. __serialize()/__unserialize()?
Mironsoft
PHP-Entwicklung, Code-Reviews und Sicherheitsaudits für Magento- und PHP-Projekte
Sichere PHP-Serialisierung in eurem Code-Stack?
Wir analysieren bestehende PHP- und Magento-Codebasen auf unsichere unserialize()-Aufrufe, Object-Injection-Risiken und ineffiziente Cache-Formate, und begleiten die Migration zu sicheren, performanten Serialisierungsstrategien.
Security-Audit
Systematische Suche nach unsicheren unserialize()-Aufrufen und Object-Injection-Angriffsflächen im Code
Migration
Schrittweise Migration von serialize()-Cache- und Sessiondaten zu JSON ohne Breaking Changes
Performance
Benchmark-basierte Empfehlungen für Cache-Formate in Redis, APCu und Message Queues