PHP 8.5.9 veröffentlicht: drei CVEs und was sie bedeuten
Sie befinden sich: Home > Webmaster News
Am 30. Juli 2026 ist PHP 8.5.9 erschienen. Im Changelog stehen diesmal drei Einträge mit CVE-Nummer, und einer davon ist eine echte SQL-Injection. Dazu kommen ein Out-of-Bounds-Write in bccomp(), ein Use-after-Free in array_multisort() und gleich vier Speicherfehler in den beiden ODBC-Erweiterungen. PHP 8.5.9 ist damit kein Update, das bis zum nächsten Wartungsfenster warten kann.
Neue Sprachfunktionen bringt das Release nicht. Dafür schließt es Fehler, die man dem Changelog-Einzeiler nicht ansieht. Ich habe jeden Punkt einzeln durchgearbeitet: was kaputt war, wann der Fehler auftrat und wie gefährlich er wirklich ist.
Sortiert ist der Artikel nach Gefährlichkeit, nicht nach der Reihenfolge im Changelog. Wer nur wissen will, ob sein System betroffen ist, springt direkt zur Tabelle am Ende.
Wie die Schweregrade in diesem Artikel zu lesen sind
Nicht jeder Fehler in PHP 8.5.9 ist eine Sicherheitslücke. Eine falsch formatierte Reflection-Ausgabe ist ärgerlich, aber nicht mit einem Heap-Overflow vergleichbar. Deshalb bekommt jeder Punkt hier eine Zahl von 1 bis 10. Das ist meine technische Einschätzung auf Basis der Issues und Commits, nicht der offizielle CVSS-Wert.
Die Skala
- 1 bis 2: kleine Funktionsfehler, schiefe Debug-Ausgaben, winzige Speicherlecks.
- 3 bis 4: deutliche Fehlfunktionen oder Abstürze unter besonderen Einstellungen.
- 5 bis 6: auslösbare Denial-of-Service-Probleme oder gebrochene Integritätsgarantien.
- 7 bis 8: native Speicherfehler, Datenlecks, kontrollierbare Speicherbeschädigung.
- 9: praktisch nutzbare Injection mit Zugriff auf fremde Daten.
- 10: breit und zuverlässig ausnutzbare Remote-Code-Execution.
Drei Punkte tragen eine offizielle CVE-Nummer. Mit denen fängt es an.
CVE-2026-17543: SQL-Injection in den PGSQL-Hilfsfunktionen
Der schwerste Fehler im ganzen Release. Betroffen sind die Komfortfunktionen pg_convert(), pg_select(), pg_insert() und pg_update(). Sie bauen ihr SQL selbst zusammen und maskierten die Werte mit PQescapeStringConn().
Diese Funktion ist für normale Stringliterale in einfachen Anführungszeichen gedacht. PHP packte ihr Ergebnis danach aber in ein Escape-Literal der Form E'...'. In einem E-String hat der Backslash eine Sonderbedeutung. Genau da klafft die Lücke. Bei aktivem standard_conforming_strings passen Maskierung und Interpretation nicht mehr zusammen. Ein Wert aus Backslash und Anführungszeichen bricht dann aus dem Literal aus.
<?php
$db = pg_connect('dbname=test');
pg_query(
$db,
'SET standard_conforming_strings = 1'
);
$params = [
'name' => "zzz\\' OR 1=1 --"
];
/* Der erzeugte Ausdruck begann mit E'
und der Wert brach daraus aus. */
echo pg_select(
$db,
'users',
$params,
PGSQL_DML_STRING
);
Die Korrektur ist winzig. Der Wrapper setzt jetzt ein einfaches ' vor den Wert statt E'. Damit passen Escape-Funktion und Literaltyp wieder zusammen. Die Tests decken alle vier Funktionen ab, dazu Manipulationsversuche an Boolean-Spalten.
Schweregrad: 9 von 10. Eine erfolgreiche SQL-Injection liest Daten aus, verändert sie oder löscht sie. Je nach Datenbankrechten lässt sich damit auch eine Anmeldung umgehen. Keine 10 gibt es nur, weil die Anwendung genau diese vier Funktionen und eine passende Serverkonfiguration verwenden muss.
Wer diese Funktionen benutzt, sollte sie ohnehin ersetzen. Sie erzeugen SQL aus Arrays, und das ist selbst nach dem Fix eine schwer prüfbare Konstruktion. Wie es sauber geht, steht im Tutorial SQL-Injection verhindern mit Prepared Statements.
CVE-2026-17544: Out-of-Bounds-Write in bccomp()
Hier wird echter Speicher außerhalb der Reservierung beschrieben. bccomp() wandelt Dezimalzahlen zuerst in die interne BCMath-Darstellung um. Wird eine Zahl auf die gewünschte Zahl der Nachkommastellen gekürzt, entfernt der Parser danach die überflüssigen Nullen am Ende.
Dabei schrumpfte die Variable str_scale, der Zeiger fractional_end blieb aber am alten Ende stehen. Die Speicherreservierung richtete sich nach der kurzen Länge, der Kopiervorgang nach der langen. Das Ergebnis ist ein Schreibzugriff hinter dem reservierten Bereich.
<?php
$n = '1.' . '9' . str_repeat('0', 300) . '1';
/* Offizieller Regressionstest: schrieb vor
dem Fix hinter den reservierten Puffer. */
var_dump(bccomp($n, '0', 300));
Nach dem Fix beschreiben str_scale und fractional_end denselben gekürzten Bereich. Die kopierte Länge kann nicht mehr größer sein als das Ziel. Bei kleinen Zahlen stammt der betroffene Speicher aus einer 256 Byte großen Stack-Arena. Es war also auch ein stackbasierter Overflow möglich.
Schweregrad: 8 von 10. Der Eingabewert kommt vollständig aus PHP. Jede Anwendung, die fremde Dezimalzahlen mit bccomp() vergleicht, konnte den Pfad aus der Ferne erreichen. Ob daraus zuverlässig Codeausführung wird, hängt an Plattform, Compiler und Speicherlayout. Eine kontrollierbare Speicherbeschädigung bleibt sie trotzdem.
Use-after-Free in array_multisort() über __toString()
Der zweite Achter im Release, und der einzige, der komplett ohne Datenbank oder Bildbibliothek auskommt. array_multisort() merkte sich Zeiger auf die Buckets der Arrays, die es gerade sortiert.
Bei einem Vergleich mit SORT_STRING kann PHP aber Stringable::__toString() aufrufen. Dieser Code gehört der Anwendung. Er darf das Array leeren, ersetzen oder komplett neu aufbauen. Der alte Speicher wurde dann freigegeben, während die Sortierfunktion noch Zeiger darauf hielt. Danach folgten Lesezugriffe im Vergleicher und Schreibzugriffe beim Neuaufbau.
<?php
class Evil
{
public function __toString(): string
{
/* Leert das Array mitten im Sortieren */
$GLOBALS['a'] = [];
return 'x';
}
}
$a = [];
for ($i = 0; $i < 10; $i++) {
$a[] = new Evil();
}
$GLOBALS['a'] = &$a;
var_dump(
array_multisort($a, SORT_STRING)
);
PHP 8.5.9 legt die tatsächlichen HashTable-Zeiger zu Beginn in einem eigenen Array ab und erhöht je Tabelle den Referenzzähler. Ein Callback darf die PHP-Variable danach beliebig verändern. Den Speicher freigeben kann er während des Sortierens nicht mehr.
Schweregrad: 8 von 10. Use-after-Free mit anschließendem Lesen und Schreiben ist immer heikel. Der Auslöser lässt sich vollständig in PHP bauen. In einer typischen Webanwendung braucht ein Angreifer allerdings Kontrolle über die sortierten Objekte oder deren __toString().
Vier Speicherfehler in ODBC und PDO_ODBC
Wer PHP mit einem ODBC-Treiber betreibt, bekommt in PHP 8.5.9 das dickste Paket. Vier Fehler auf einmal, drei davon im Bereich sieben bis acht. Der Ausgangspunkt ist bei allen ähnlich: PHP vertraute Längenangaben, die der Treiber meldet, ohne sie gegen die eigene Pufferkapazität zu prüfen.
Am gefährlichsten ist GH-22666. PDO reservierte den Ausgabepuffer nach dem maxlen aus bindParam(). Kopiert wurde dann aber die echte Länge des aktuellen Werts. Ein 64 Byte langer String landete so in einem vier Byte großen Puffer.
<?php
$pdo = new PDO(
'odbc:Driver=SQLite3;Database=:memory:'
);
$value = str_repeat('A', 64);
$stmt = $pdo->prepare('SELECT ?');
/* maxlen = 4, Wert = 64 Byte:
das schrieb vor dem Fix in den Heap. */
$stmt->bindParam(
1,
$value,
PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT,
4
);
$stmt->execute();
var_dump($value); /* jetzt: string(4) "AAAA" */
Die Parameterstruktur hat jetzt ein eigenes Feld outbuflen mit der wirklichen Kapazität. Sowohl das Hineinkopieren als auch das spätere Zurücklesen sind darauf begrenzt. Schweregrad: 8 von 10, weil der Schreibzugriff kontrollierbar ist und die Quelldaten aus PHP stammen.
GH-22668 und GH-22667 sind das Gegenstück in Leserichtung, einmal in der klassischen ODBC-Erweiterung und einmal in PDO_ODBC. Ein Treiber darf einen zu langen Spaltenwert im Puffer abschneiden und trotzdem die volle ursprüngliche Länge melden. PHP nahm diese volle Länge und kopierte damit Bytes hinter dem Puffer in den Rückgabestring. Angrenzende Heap-Daten landeten so direkt in einer PHP-Variablen. Das ist neben dem Absturzrisiko ein handfestes Informationsleck, Schweregrad 7 von 10 für beide.
GH-22665 betrifft den Diagnosepuffer. SQLGetDiagRec() darf eine Fehlermeldung kürzen und die volle Länge melden. PHP setzte das abschließende NUL-Byte genau an diesen gemeldeten Index. Bei einer Meldung von mehr als 511 Byte landete es hinter dem Puffer. Meist wird dabei nur ein einzelnes Byte geschrieben, deshalb Schweregrad 7 von 10 statt acht.
Der vierte Fehler, GH-20726, ist harmloser, aber gut reproduzierbar. Enthält der DSN weder Benutzername noch Passwort, speichert PDO beide Felder als NULL. Bei aktivem unixODBC-Connection-Pooling vergleicht der Driver Manager Zugangsdaten mit strcmp(), und strcmp(NULL, ...) beendet den Prozess. PHP 8.5.9 reicht jetzt leere Strings statt Nullzeiger weiter. Schweregrad: 4 von 10.
Wer statt ODBC lieber den nativen Treiber nutzt, ist von allen vier Punkten nicht betroffen. Der Vergleich der Zugriffswege steht im Tutorial PDO: sichere Datenbankzugriffe mit Prepared Statements.
CVE-2026-9672: libgd und die GIF-Dekomprimierung
PHP 8.5.9 aktualisiert die eingebettete libgd. Der Grund sind Fehler in der LZW-Dekomprimierung von GIF-Dateien. Eine Initialisierung schrieb bei der zweiten LZW-Tabelle immer auf Index 0, statt den laufenden Index i zu verwenden. Der Decoder arbeitete außerdem nach einem ungültigen Teilcode einfach weiter.
Der Einstiegspunkt dafür ist in vielen Anwendungen offen. Ein hochgeladenes GIF genügt.
<?php
$gif = file_get_contents('fremdes-bild.gif');
/* Lief vor dem Fix mit inkonsistenten
LZW-Tabellen weiter. */
$image = @imagecreatefromstring($gif);
var_dump($image);
Nach dem Update wird die Decoderstruktur vollständig mit Nullen initialisiert, und ein fehlerhafter Teilcode bricht sofort mit einem Fehler ab. Schweregrad: 7 von 10. Bilddecoder verarbeiten direkt hochgeladene Dateien und laufen als nativer C-Code im Webprozess. Ein Absturz war sicher möglich. Für zuverlässige Codeausführung gibt es in den veröffentlichten Patchinformationen keinen Beleg, deshalb keine 8 oder 9.
Wer Uploads mit GD weiterverarbeitet, findet die Grundlagen im Tutorial GD-Bibliothek: Bilder programmatisch erzeugen und bearbeiten.
CVE-2026-7260: Phar und zyklische Symlinks
PHAR und PharData folgen Symlinks in TAR-Archiven. Die alte Funktion phar_get_link_source() rief sich für jedes Linkziel selbst wieder auf. Bei einem Kreis aus a nach b und b zurück nach a gab es keinen Abbruch. Der Stack wuchs, bis der Prozess starb.
Die Rekursion ist jetzt einer iterativen Verfolgung gewichen. Zur Kreiserkennung nutzt PHP Floyds Tortoise-and-Hare-Algorithmus: ein langsamer Zeiger folgt pro Runde einem Link, ein schneller zwei. Treffen sich beide, liegt ein Zyklus vor und die Funktion liefert NULL. Getestet wird ein Zweierkreis, ein Kreis mit Vorlauf und einer mit 400 Einträgen.
Schweregrad: 6 von 10. Ein fremdes TAR- oder PHAR-Archiv konnte den verarbeitenden Prozess beenden. Das ist Denial of Service, keine nachgewiesene Codeausführung.
Im selben Aufwasch hat PHP 8.5.9 die Behandlung des magischen Verzeichnisses .phar vereinheitlicht. Vorher wurden Namen wie .pharfile oder .phar-dir je nach Codepfad unterschiedlich bewertet: bei der Dateierstellung akzeptiert, beim Kopieren abgelehnt, bei der Extraktion wieder anders. Geschützt bleibt nur noch der exakte Pfad /.phar. Schweregrad: 4 von 10, denn uneinheitliche Pfadregeln in Archivformaten sind der Stoff, aus dem Sicherheitsprüfungen ins Leere laufen.
DBA, ZIP und ein gebrochenes readonly
Drei Fehler aus ganz unterschiedlichen Ecken, die alle im mittleren Bereich landen. Gemeinsam ist ihnen, dass sie erst durch fremde Daten oder ungewöhnliche Abläufe gefährlich werden.
Der Flatfile-Handler von DBA speichert Längenangaben als Text. Ein manipulierter Wert wie -1 wurde mit atoi() gelesen und dann in einen vorzeichenlosen size_t gewandelt. Aus -1 wird dabei ein riesiger positiver Wert, und die anschließende Addition von 1024 lief zusätzlich über. Der Parser las danach über das Pufferende hinaus. PHP 8.5.9 prüft die Länge vor jeder Puffervergrößerung. Schweregrad: 6 von 10, sofern die geöffnete Datenbankdatei aus fremder Hand stammt.
In der ZIP-Erweiterung steckte ein Use-after-Free beim Kommentar. Ein Eintrag kann einen aus dem ursprünglichen Archiv geerbten Kommentar tragen. Wurde der Eintrag überschrieben und danach exakt derselbe Kommentar erneut gesetzt, gab libzip den alten Bereich frei und griff gleichzeitig noch darauf zu.
<?php
$zip = new ZipArchive();
$zip->open('test.zip', ZipArchive::CREATE);
$zip->addFromString('dir/entry.txt', 'original');
$zip->setCommentName('dir/entry.txt', 'dir/entry.txt');
$zip->close();
$zip->open('test.zip');
$zip->addFromString('dir/entry.txt', 'updated');
/* Derselbe geerbte Kommentar erneut:
das war der Ausloeser. */
var_dump(
$zip->setCommentName('dir/entry.txt', 'dir/entry.txt')
);
PHP liest den aktuellen Kommentar jetzt zuerst aus. Stimmen Länge und Inhalt bereits, kommt sofort true zurück und libzip bekommt gar keinen Set-Aufruf. Schweregrad: 6 von 10. Der Ablauf braucht ein vorhandenes Archiv, einen überschriebenen Eintrag und genau diesen Kommentar. Die wahrscheinlichste Folge war ein Absturz.
Zweiter ZIP-Punkt: ZipArchive::FL_UNCHANGED wurde bei gelöschten Einträgen teilweise ignoriert. getFromIndex() reichte das Flag zwar an zip_fopen_index() weiter, fragte die Eintragsgröße davor aber ohne Flags ab. Bei einem gelöschten Eintrag scheiterte schon diese Abfrage. Schweregrad: 2 von 10. Mehr zur Arbeit mit Archiven steht im Tutorial ZipArchive: ZIP-Dateien erstellen, bearbeiten und entpacken.
Der dritte Punkt ist der interessanteste für modernen Anwendungscode. PHP 8.5 erlaubt das Ändern einzelner Eigenschaften direkt beim Klonen. Behandelt wurden dabei aber nur die tatsächlich überschriebenen Eigenschaften. Andere readonly-Eigenschaften des Klons blieben in einem erneut beschreibbaren Zustand.
<?php
readonly class Test
{
public public(set) int $a;
public public(set) int $b;
public function __construct()
{
$this->a = 1;
$this->b = 2;
}
}
$test = clone(new Test(), ['a' => 3]);
/* Darf nach dem Klonen nicht mehr gehen,
war vor dem Fix aber moeglich. */
$test->b = 4;
Die Engine läuft nach dem Klonen jetzt durch alle deklarierten Eigenschaften und entfernt das interne Flag IS_PROP_REINITABLE. Schweregrad: 5 von 10. Kein Speicherfehler, aber ein Bruch der zentralen Zusage von readonly. Wer Berechtigungen, signierte Zustände oder unveränderliche Wertobjekte darauf stützt, hatte plötzlich veränderbare Objekte in der Hand.
DOM: Stack-Overflow bei tiefen Bäumen
Die XML-Serialisierung lief rekursiv durch jedes Kind eines Knotens. Ein Dokument mit zehntausenden ineinander verschachtelten Elementen erzeugte damit ebenso viele C-Funktionsaufrufe. Der C-Stack war erschöpft, bevor PHP kontrolliert abbrechen konnte.
<?php
$document = Dom\XMLDocument::createEmpty();
$node = $document->appendChild(
$document->createElement('root')
);
for ($i = 0; $i < 100000; $i++) {
$node = $node->appendChild(
$document->createElement('a')
);
}
/* Jetzt: sauberer Fehler statt Absturz */
echo $document->saveXml();
Der Serialisierer prüft beim Eintritt in den rekursiven Dispatcher nun das Stack-Limit der Zend Engine. Bei Überschreitung entsteht ein normaler Error mit der Meldung Maximum call stack size reached. Eine zweite Warnung oder eine falsche Folge-Exception danach unterbleibt. Schweregrad: 5 von 10, ein brauchbarer Denial-of-Service-Pfad ohne belegten Speicher-Write.
Der zweite DOM-Fehler ist harmlos, aber ärgerlich. NodeLists werden teilweise verzögert ausgewertet und merken sich den zuletzt besuchten Knoten. Bei getElementsByClassName() startete die Suche zwar am gecachten Knoten, bewegte sich aber nicht korrekt zum nächsten Treffer. Ein Sprung auf einen kleineren Index lieferte dadurch das falsche Element. Schweregrad: 2 von 10. Wer viel mit XML arbeitet, findet die Grundlagen im XML-Tutorial: XML-Daten lesen, parsen und erstellen.
Opcache und JIT: drei Abstürze weniger
Die JIT-Fixes in PHP 8.5.9 sind weniger dramatisch als im Frühjahr. Falsche Rechenergebnisse gibt es diesmal keine. Dafür drei Wege in den Absturz, die alle bestimmte Aufrufmuster brauchen.
GH-22158 betrifft megamorphe Aufrufe, also Aufrufstellen, an denen zur Laufzeit viele verschiedene Funktionen landen. Beim Zusammenführen der Dispatchpfade waren die Operanden einer internen PHI-Operation vertauscht. Der ausgewählte Funktionszeiger passte danach nicht mehr zum ausgewählten Runtime-Cache-Slot. Der JIT konnte den Observer-Handler über den Slot einer internen statt einer Benutzerfunktion aufrufen. Ergebnis: Nullzeigeraufruf und Prozessabsturz. Schweregrad: 4 von 10, da Tracing-JIT plus Observer plus genau dieses Muster nötig sind.
GH-22443 liegt daneben. Bei dynamischen Aufrufen behandelte der JIT ein unbekanntes Ziel teilweise so, als sei es sicher eine Closure. Der vorliegende zend_map_ptr-Offset wurde nicht gegen die globale map_ptr_base aufgelöst und später wie eine echte Adresse dereferenziert. Der direkte Closure-Pfad greift jetzt nur noch, wenn der JIT statisch weiß, dass das Ziel eine Closure ist. Schweregrad: 4 von 10.
GH-21770 trifft alle, die Preloading und Property Hooks kombinieren. Beim Vorladen ging bei einer aus einem Trait übernommenen Hook-Methode die Zuordnung prop_info verloren. Ohne diese Information erkannte die Engine den Zugriff im eigenen Getter nicht mehr als direkten Zugriff und rief den Getter erneut auf.
<?php
trait T
{
public string $a {
get => $this->a;
}
}
class C
{
use T;
}
$c = new C();
$c->a = 'test';
/* Ohne Preloading korrekt.
Mit vorgeladenem Trait: endlose Rekursion. */
echo $c->a;
Beim Kopieren der op_array bleibt prop_info jetzt erhalten. Schweregrad: 4 von 10. Der Fehler beendete den Prozess, brauchte aber Preloading, einen Trait und Property Hooks in genau dieser Form.
Der Rest: Reflection, Intl, Datum und URLs
Der längere Teil des Changelogs besteht aus Korrekturen, die niemanden nachts wecken. Sie machen PHP 8.5.9 trotzdem vorhersagbarer, vor allem beim Debuggen.
Gleich fünf Reflection-Punkte hängen an NUL-Bytes und Namensräumen. ReflectionConstant::__toString() gab String-Werte über einen C-String-Helfer aus und endete deshalb am ersten NUL-Byte. Aus "f\0oo" wurde in der Ausgabe ein einzelnes f. Dasselbe Muster steckte in weiteren Ausgaben: Dokumentationskommentare, dynamische Eigenschaftsnamen, INI-Werte, Konstantennamen. Anonyme Klassen tragen sogar regulär ein NUL-Byte im internen Namen. Alle betroffenen Helfer nehmen jetzt zend_string* statt char* entgegen und kennen damit die echte Länge.
Dazu kommen zwei Kleinigkeiten: ReflectionParameter akzeptiert jetzt einen führenden Backslash im Funktionsnamen, und ReflectionClass::hasProperty() findet wieder dynamische Eigenschaften, die eine private Elterneigenschaft gleichen Namens verdecken. Vorher meldete Reflection dort schlicht, die Eigenschaft existiere nicht. Alles zwischen 1 und 2 von 10, aber genau die Sorte Fehler, die Serializer und Frameworks in die Irre schickt.
In der Intl-Erweiterung liefert Locale::lookup() jetzt null, wenn ein Sprach-Tag gar nicht kanonisierbar ist. Vorher kam in diesem Fall der Fallback zurück, und die Anwendung konnte einen ungültigen Tag nicht mehr von einem unbekannten unterscheiden. Zusätzlich sind drei Speicherlecks weg: Collator, Spoofchecker und IntlListFormatter ersetzten bei einem zweiten Konstruktoraufruf ihren ICU-Handle, ohne den alten freizugeben. Ein solcher Zweitaufruf ist über Reflection möglich und wird jetzt mit einem Error abgewiesen. Und erfolgreiche IntlChar-Aufrufe lassen keinen alten globalen Fehlerstatus mehr stehen.
Bei den Datumsfunktionen hebt PHP 8.5.9 Timelib auf 2022.17. Damit verschwindet ein alter Parser-Fehler: eine Eingabe aus genau einem Leerzeichen wurde als Fehler behandelt, weil die Schleife zum Entfernen von Leerraum nur bei s < e lief. Bei einem einzigen Zeichen waren beide Zeiger schon gleich. Außerdem berechnet Timelib negative Zeitstempel im Februar des astronomischen Jahres 0 nicht mehr über Sonderfälle, sondern über eine allgemeine Umrechnung von Epochentagen. Praktisch relevant ist das nur für historische und astronomische Daten.
Ein Datum-Fix betrifft dagegen jeden, der eigene Datumsklassen schreibt: __debugInfo() in einer Unterklasse von DateTime, DateTimeImmutable, DateTimeZone, DateInterval oder DatePeriod wurde bisher ignoriert. var_dump() zeigte stattdessen die internen Felder. Die Property-Handler unterscheiden jetzt den Zweck ZEND_PROP_PURPOSE_DEBUG und rufen die eigene Methode auf.
Bei den URI-Klassen sind drei Punkte zusammengekommen. Eine URL wie scheme:// hat ein leeres Hostfeld, das Lexbor teilweise als opaquen Host der Länge null ablegte statt als ausdrücklich leeren Host. Die Wither-Methoden konnten dadurch Benutzername, Passwort oder Port setzen, wo das gar nicht erlaubt ist. Zweitens fehlte das Caret ^ im Percent-Encode-Set für Pfade, obwohl der WHATWG-Standard es dort verlangt. Aus /p^th muss /p%5Eth werden. Drittens meldete der Parser bei leerem Host mit Zugangsdaten den Fehlercode INVALID_CREDENTIALS, obwohl der fehlende Host das eigentliche Problem ist. Alles zwischen 1 und 3 von 10, aber in Signatur-, Cache- und Allowlist-Systemen kann eine falsch normalisierte URL sehr wohl den Unterschied machen.
Aus der Standard-Bibliothek kommt neben array_multisort() noch ein stiller Datenverlust. base_convert() schnitt große Ergebnisse nach 64 Zeichen ab, weil der Ausgabepuffer aus der Bitbreite eines double berechnet wurde statt aus dessen maximaler Textlänge.
<?php
$result = base_convert(
str_repeat('1', 61),
36,
16
);
/* Korrekt sind 78 Zeichen,
vorher kamen 64 zurueck. */
var_dump(strlen($result));
Schweregrad: 3 von 10. Der Fehler verfälschte Kennungen und Konvertierungsergebnisse, ohne sich zu melden. Für kryptografische Ganzzahlrechnungen taugt base_convert() ohnehin nicht, weil es bei solchen Größen über Gleitkommazahlen arbeitet.
Der Rest in Stichworten: SPL akzeptiert bei class_parents(), class_implements() und class_uses() jetzt auch führende Backslashes, unabhängig vom Autoload-Parameter. SplFileObject::fputcsv() auf einem ohne Konstruktor erzeugten Objekt liefert einen normalen PHP-Fehler statt eines Segfaults. Der Session-Garbage-Collector zieht seine Zufallsquelle erst kurz vor der ersten Auslosung statt beim Start der Erweiterung. exif_read_data() warnt nicht mehr fälschlich über eine Illegal IFD size, wenn ein gültiges IFD einfach ohne Folgezeiger endet. Der XXH3-Hash reserviert seinen Kontext jetzt mit der benötigten 64-Byte-Ausrichtung, statt sich auf den Speicherverwalter zu verlassen. gregoriantojd() und juliantojd() prüfen vor der internen Addition von 4800 auf einen Integer-Überlauf. Und der SSL-Stream-Wrapper blockiert nicht mehr bis zum vollen Timeout, wenn er nach einem erfolgreichen Read nur noch nachschauen will, ob weitere Daten anliegen.
Kritikalität auf einen Blick
Die Tabelle fasst zusammen, wie ich die einzelnen Punkte einordne. Die offiziellen Release-Notes enthalten keine Schweregrade, die Bewertung stammt von mir.
| Bereich |
Fehler |
Schweregrad |
| PGSQL |
CVE-2026-17543, SQL-Injection über E'...' |
9 |
| BCMath |
CVE-2026-17544, Out-of-Bounds-Write in bccomp() |
8 |
| Standard |
GH-22678, Use-after-Free in array_multisort() |
8 |
| PDO_ODBC |
GH-22666, Heap-Overflow bei Output-Parametern |
8 |
| ODBC, PDO_ODBC |
GH-22668, GH-22667, Heap-Over-Read bei langen Spaltenwerten |
7 |
| PDO_ODBC |
GH-22665, Out-of-Bounds-Write im Diagnosepuffer |
7 |
| GD |
CVE-2026-9672, libgd-Fehler in der GIF-Dekomprimierung |
7 |
| Phar |
CVE-2026-7260, Absturz durch zyklische Symlinks |
6 |
| DBA |
Out-of-Bounds-Read im Flatfile-Handler |
6 |
| ZIP |
GH-22649, Use-after-Free beim Zurücksetzen eines Kommentars |
6 |
| Core |
readonly-Eigenschaften nach clone with wieder sperren |
5 |
| DOM |
GH-22570, Stack-Overflow beim Serialisieren tiefer Bäume |
5 |
| Opcache |
GH-22158, GH-22443, GH-21770, drei JIT- und Preload-Abstürze |
4 |
| Phar |
Einheitliche Behandlung des magischen Verzeichnisses .phar |
4 |
| PDO_ODBC, Hash |
GH-20726 Pooling-Absturz, GH-18173 falsche Speicherausrichtung |
4 |
| Standard, Calendar, URI |
base_convert()-Abschnitt, Integer-Überlauf, leerer opaquer Host |
3 |
| Reflection, Intl, Date, SPL, ZIP, Session, Exif |
NUL-Bytes in Ausgaben, Speicherlecks, Parser- und API-Konsistenz |
1 bis 2 |
Auffallend ist die Verteilung. Die schweren Punkte hängen fast alle an fremden Daten: an Dezimalzahlen aus einem Formular, an einer Datenbankantwort, an einer hochgeladenen GIF-Datei, an einem TAR-Archiv.
Was jetzt zu tun ist
PHP 8.5.9 einspielen ist die kurze Antwort. Wer wissen will, wie dringend das im eigenen Fall ist, geht diese Punkte durch.
1
PostgreSQL im Einsatz? Sofort updaten.
Suchen Sie im Code nach pg_select, pg_insert, pg_update und pg_convert. Findet sich auch nur eine dieser Funktionen mit Werten aus einer Anfrage, ist das Update dringend. Ohne Update hilft kurzfristig nur, die betroffenen Aufrufe auf Prepared Statements umzustellen.
2
BCMath mit fremden Zahlen? Ebenfalls dringend.
Preisvergleiche, Währungsrechner und Buchhaltungslogik greifen gern zu bccomp(). Kommt der Vergleichswert aus einem Formular oder einer API, war der Out-of-Bounds-Write erreichbar. Eine Eingabeprüfung auf sinnvolle Nachkommastellen ist auch nach dem Update kein Fehler.
3
Bild-Uploads prüfen.
GIF-Dateien landen über jedes Upload-Formular im nativen Decoder. Wer Uploads mit GD oder getimagesize() verarbeitet, sollte PHP 8.5.9 zeitnah einspielen und nebenbei prüfen, ob die erlaubten Dateitypen wirklich so breit sein müssen.
4
Archive aus fremder Hand.
Wer TAR-, PHAR- oder ZIP-Dateien von Nutzern entgegennimmt, hatte mit den Symlink-Kreisen und dem ZIP-Kommentarfehler zwei Absturzwege im Haus. Beides ist behoben, das Entpacken in ein isoliertes Verzeichnis bleibt trotzdem Pflicht.
5
Nach dem Update kurz hinschauen.
Der readonly-Fix, die DOM-Stack-Prüfung und die neue null-Rückgabe von Locale::lookup() ändern Verhalten, das vorher falsch war. Code, der sich versehentlich darauf verlassen hat, meldet sich jetzt. Ein Blick ins Fehlerlog für ein paar Stunden genügt.
Die Quellpakete, die Windows-Builds und die Signaturen liegen wie immer bei php.net bereit. Wer selbst kompiliert oder ein eigenes Docker-Image baut, holt sich das Archiv direkt dort ab.
Häufige Fragen zu PHP 8.5.9
Diese Punkte kommen erfahrungsgemäß nach jedem Sicherheitsrelease auf.
Bin ich betroffen, wenn ich nur MySQL nutze?
Die SQL-Injection trifft ausschließlich die PGSQL-Erweiterung. Der BCMath-Fehler, der Use-after-Free in array_multisort() und die GIF-Lücke sind aber unabhängig von der Datenbank. Ein Update auf PHP 8.5.9 lohnt sich also auch ohne PostgreSQL.
Gibt es Breaking Changes?
Neue Sprachfeatures oder entfernte Funktionen bringt PHP 8.5.9 nicht. Zwei Verhaltensänderungen sollten Sie kennen: Locale::lookup() liefert bei nicht kanonisierbaren Sprach-Tags jetzt null statt des Fallbacks, und ein zweiter Konstruktoraufruf auf Collator, Spoofchecker oder IntlListFormatter wirft einen Error.
Reicht es, JIT abzuschalten?
Nein. Anders als im Release 8.5.5 stecken die schweren Fehler diesmal nicht im JIT. Die drei Opcache-Punkte in PHP 8.5.9 sind Abstürze mit engen Voraussetzungen. Die CVEs erreichen Sie auch mit deaktiviertem JIT.
Mein Hoster liefert noch 8.5.8. Was tun?
Fragen Sie nach einem Termin für das Update und nennen Sie die drei CVE-Nummern. Bis dahin hilft nur, die betroffenen Pfade zu meiden: PGSQL-Komfortfunktionen ersetzen, Uploads einschränken, bccomp()-Eingaben prüfen.
Wie finde ich heraus, welche Version läuft?
Auf der Kommandozeile genügt php -v. Im Web liefert phpversion() denselben Wert, ohne wie phpinfo() die halbe Serverkonfiguration offenzulegen. Denken Sie daran, dass CLI und Webserver oft unterschiedliche PHP-Versionen benutzen.
Fazit
PHP 8.5.9 ist ein Sicherheitsrelease, auch wenn der Changelog es nicht so nennt. Drei CVEs, dazu ein Use-after-Free in einer Standardfunktion und vier Speicherfehler in den ODBC-Erweiterungen sind mehr, als ein normales Wartungsupdate mitbringt.
Besonders unangenehm ist die Kombination aus SQL-Injection und einer Funktionsfamilie, die viele Projekte aus Bequemlichkeit einsetzen. Wer pg_insert() und Geschwister im Code hat, sollte nicht nur updaten, sondern die Stellen gleich umbauen.
Der Rest des Releases ist unspektakulär und trotzdem nützlich. Reflection zeigt wieder vollständige Werte, Intl verliert keinen Speicher mehr, das Datumsmodul parst Randfälle korrekt. Nichts davon rechtfertigt allein ein Update. Zusammen mit den CVEs ergibt es aber eine klare Empfehlung: einspielen, kurz testen, Logs beobachten.
Jetzt weiter zum Tutorial: SQL-Injection in PHP zuverlässig verhindern
(Autor:
schubertmedia), Eingetragen am 05.08.2026