bk99.de entertain the web since 1997

PHP2Shell: Updates einspielen und bis dahin Multipart an Caddy und Apache sperren

Zusammenfassung

Am 21. Oktober 2026 hat das PHP-Team mit 8.5.12, 8.4.27, 8.3.36 und 8.2.35 zwei kritische Lücken geschlossen, über die Angreifer ohne Anmeldung Code ausführen können. Ich zeige, wie man prüft, ob das Update wirklich angekommen ist, und wie man Server bis dahin mit einer Multipart-Sperre an Caddy oder Apache und den offiziellen PHP-Einstellungen absichert. Dazu gehört ehrlich, was diese Notlösungen kosten, was nicht hilft und was nach dem Update bleiben sollte.

Was jetzt bestätigt ist

Das PHP-Team hat am 21. Oktober 2026 die Versionen 8.5.12, 8.4.27, 8.3.36 und 8.2.35 veröffentlicht. Sie beheben zwei Lücken, beide mit CVSS v4.0 9.2 bewertet: CVE-2026-109618 und CVE-2026-109619. Gefährdet ist laut PHP jede Installation, die Webanfragen bedient, also PHP-FPM, Apache mit mod_php, php-cgi, FrankenPHP und LiteSpeed. Das größte Risiko sieht PHP bei mod_php, dort gibt es einen vorgeführten Angriff ohne Anmeldung, der greift, bevor Anwendungscode läuft.

Die erste Lücke betrifft Speichermangel. Stößt PHP an memory_limit, bricht es die Anfrage ab, während interne Daten erst halb aufgebaut sind, und das Aufräumen behandelt diese Daten danach als gültig. Auslösen lässt sich das unter anderem mit präparierten multipart-Uploads: ohne verwertbare Längenangabe, mit sehr vielen Kopfzeilen je Teil oder mit leeren Dateifeldern unter verschachtelten Namen. Die zweite Lücke betrifft Zustand, der eine abgebrochene Anfrage überlebt. Ein späterer Request im selben Prozess benutzt ihn, bevor PHP ihn zurücksetzt. Bestätigt sind dafür Warnungen aus Einstellungen, die pro Anfrage gesetzt werden, und die Upload-Fortschrittsanzeige der Sessions, gegen mod_php und auch gegen PHP-FPM.

Die Berichte vor dem Release lagen damit in der Richtung richtig, aber zu eng: multipart ist ein bestätigter Auslöser, nicht der einzige. Speichermangel kann auch aus der Anwendung selbst kommen, und für die zweite Lücke genügt jeder fatale Fehler, etwa eine Zeitüberschreitung, an der richtigen Stelle.

Was du mitnehmen kannst: Nur das Update schließt beide Lücken vollständig. Alles Weitere in diesem Beitrag verkleinert die Angriffsfläche bis dahin und ersetzt das Update nicht.

Prüfen, ob das Update wirklich da ist

Distributionen haben die Patches vorab bekommen und übernehmen sie in ihre eigenen Pakete. Dabei bleibt die Versionsnummer oft stehen. Ubuntu 24.04 liefert den Fix zum Beispiel als 8.3.6-0ubuntu0.24.04.13, php -v zeigt weiter 8.3.6. Entscheidend ist die Paketversion und der Changelog-Eintrag zu den beiden CVE-Nummern, nicht die PHP-Version.

Freigabe und Verfügbarkeit fallen auseinander. Am 21. Oktober stand das Ubuntu-Paket um 15:41 Uhr MESZ als veröffentlicht in Launchpad, auf den Spiegelservern war es rund eine Stunde später abrufbar. Wer in dieser Zeit apt-get upgrade aufruft, bekommt kein Paket und glaubt leicht, es gebe noch keins:

apt-get update
apt-cache policy php8.3-fpm
apt-get changelog php8.3-fpm | head -20

Bei Containern zählt das Image, nicht der Container. Ein Neustart des alten Images ändert nichts, und ein neues Tag heißt noch nicht, dass PHP darin gepatcht ist. Am Nachmittag des 21. Oktober enthielt etwa das am Morgen neu gebaute Image dokuwiki/dokuwiki:stable noch PHP 8.4.26, und die offiziellen Tags php:8.4.27-apache und php:8.5.12-apache gab es im Docker Hub noch nicht. Die Version steht als Umgebungsvariable im Image:

docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' dokuwiki/dokuwiki:stable | grep PHP_VERSION
docker exec <container> php -v

PHP 8.1 und älter bekommen vom PHP-Projekt keine Updates mehr. Wegen der Schwere hat das Sicherheitsteam aber minimale Quell-Backports für 7.4, 8.0 und 8.1 bereitgestellt, gedacht für alle, die selbst bauen. Wer das freiwillig gepflegte Repository rpms.remirepo.net nutzt, bekommt dort laut PHP am Tag des Releases keine Updates und soll auf Distributionspakete oder eigene Builds wechseln.

Was du mitnehmen kannst: Prüfe nach dem Update in jedem laufenden Dienst die Paketversion bzw. php -v im Container und starte jeden PHP-FPM-Dienst, Apache mit mod_php und dauerhaft laufende Worker neu. Ein installiertes Paket schützt nicht, solange der alte Prozess weiterläuft.

Warum die Sperre am Proxy weiter sinnvoll ist

Bis das Update auf allen Systemen läuft, bleibt die Sperre am Webserver oder Reverse Proxy die wirksamste schnelle Maßnahme gegen den bestätigten Upload-Weg. PHP zerlegt einen multipart-Body, bevor die Anwendung eine Zeile ausführt. Ein Login, eine Wartungsseite oder eine Prüfung im Code kommt deshalb zu spät. Der Proxy dagegen sieht den Header Content-Type und kann jede Anfrage mit „multipart“ abweisen, ohne den Body zu lesen.

Das PHP-Team empfiehlt in seinem Hinweis genau das: Anwendungen, die keine multipart-Daten brauchen, sollen solche Anfragen schon am Frontend blockieren. Die Sperre ist bewusst grob. Sie sucht nicht nach verdächtigen Feldern, denn dafür müsste der Proxy den Body zerlegen, und zwar genau so wie PHP. Sie gilt für alle HTTP-Methoden und unabhängig von Groß- und Kleinschreibung, weil PHP beides akzeptiert.

Was du mitnehmen kannst: Die Proxy-Sperre deckt den Upload-Weg ab, nicht die ganze Lücke. Speichermangel aus der Anwendung und fatale Fehler auf anderen Wegen bleiben, deshalb gehören die PHP-Einstellungen weiter unten dazu.

Caddy: multipart vor PHP abweisen

In Caddy genügt ein wiederverwendbarer Baustein, ein sogenanntes Snippet. Er kommt ganz oben ins Caddyfile und wird in jedem Site-Block importiert, der an PHP weiterreicht, egal ob per reverse_proxy oder php_fastcgi:

(php2shell_block) {
	# Notlösung bis zum PHP-Update: kein multipart zu PHP
	@multipart header_regexp Content-Type (?i)multipart
	respond @multipart "Uploads voruebergehend gesperrt" 403
}

example.org {
	import php2shell_block
	reverse_proxy php-app:80
}

Der Matcher header_regexp prüft alle Werte des Headers, (?i) schaltet die Groß- und Kleinschreibung ab. Caddy arbeitet respond in seiner festen Reihenfolge vor reverse_proxy und php_fastcgi ab. Die Anfrage erreicht PHP also nicht. Blöcke, die nur umleiten (redir), brauchen den Import nicht.

Wer einzelnen Redaktionsrechnern den Upload erlauben muss, nimmt sie im Matcher aus. Das verkleinert den Schutz, denn genau diese Adressen bleiben angreifbar:

@multipart {
	header_regexp Content-Type (?i)multipart
	not client_ip 192.0.2.10
}

Prüfen und ohne Neustart übernehmen:

caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

Läuft Caddy in Docker und ist das Caddyfile als einzelne Datei eingebunden, ändere die Datei an Ort und Stelle. Viele Editoren schreiben eine neue Datei und benennen sie um. Der Container sieht dann weiter die alte.

Was du mitnehmen kannst: Ein Snippet mit import hält die Sperre an einer Stelle. Nach dem Update löschst du die import-Zeilen und den Baustein, und alles ist wie vorher.

Apache: dieselbe Sperre

In Apache 2.4 erledigt das ein <If>-Block im VirtualHost. Wichtig ist die Variable: %{HTTP:Content-Type} ist der Header der Anfrage. %{CONTENT_TYPE} meint dagegen den Typ der Antwort und hilft hier nicht.

<VirtualHost *:443>
    # ... bestehende Konfiguration ...

    # Notlösung bis zum PHP-Update: kein multipart zu PHP
    <If "%{HTTP:Content-Type} =~ /multipart/i">
        Require all denied
    </If>
</VirtualHost>

Apache wertet <If>-Abschnitte nach <Directory> und <Location> aus. Ein „Require all granted“ an anderer Stelle hebt die Sperre deshalb nicht auf. Das gilt gleichermaßen für mod_php, PHP-FPM über proxy_fcgi und Apache als Reverse Proxy vor einem PHP-Container.

Ohne Zugriff auf die Serverkonfiguration, etwa beim Webhosting, geht es in der .htaccess mit mod_rewrite:

RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart [NC]
RewriteRule ^ - [F]

Danach mit apachectl configtest prüfen und mit apachectl graceful neu laden. Laufende Verbindungen bleiben dabei bestehen.

Was du mitnehmen kannst: Teste die Sperre von außen, nicht nur die Syntax. Eine Regel im falschen VirtualHost oder eine .htaccess, die wegen AllowOverride nicht gelesen wird, meldet keinen Fehler und schützt trotzdem nicht.

Testen, ob die Sperre greift

Drei Anfragen genügen: ein echter Upload, eine Variante mit anderer Methode und Großbuchstaben, und ein normaler Seitenaufruf, der weiter funktionieren muss.

curl -s -o /dev/null -w '%{http_code}\n' -F x=1 https://example.org/
curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H 'Content-Type: MULTIPART/form-data; boundary=a' --data x https://example.org/
curl -s -o /dev/null -w '%{http_code}\n' https://example.org/

Die ersten beiden müssen 403 liefern, der dritte den üblichen Status, also 200 oder eine Weiterleitung. Prüfe außerdem, ob der PHP-Server noch auf anderem Weg erreichbar ist, etwa über einen veröffentlichten Container-Port. Dort wirkt die Sperre nicht.

Was du mitnehmen kannst: Ein erfolgreicher Reload beweist nur, dass die Konfiguration gültig ist. Erst die 403 auf den Upload zeigt, dass sie wirkt.

Zweite Schicht: die PHP-Einstellungen aus dem offiziellen Hinweis

Für Server, die noch nicht aktualisiert werden können, nennt das PHP-Team mehrere Einstellungen. Jede schließt nur bestimmte Angriffswege, keine macht eine Installation sicher. Am einfachsten ist eine eigene Datei, die nach allen anderen geladen wird:

; /etc/php/8.x/fpm/conf.d/99-php2shell.ini
; bzw. /usr/local/etc/php/conf.d/zz-php2shell.ini in Docker-Images
session.upload_progress.enabled = 0
max_input_nesting_level = 8
post_max_size = 8M          ; 32M, wenn Uploads nötig sind
fatal_error_backtraces = 0  ; nur PHP 8.5
memory_limit = -1           ; nur zusammen mit einer Speichergrenze am Container
  • session.upload_progress.enabled = 0 trennt den bestätigten Session-Weg der zweiten Lücke. Die Einstellung muss vor dem Zerlegen der Anfrage gelten, also in php.ini, im FPM-Pool oder in der Serverkonfiguration. ini_set() und eine .user.ini reichen laut PHP nicht.
  • fatal_error_backtraces = 0 ist nur für PHP 8.5 nötig, dort ist es ab Werk eingeschaltet und Grundlage des bestätigten Angriffs auf mod_php.
  • memory_limit = -1 nimmt der ersten Lücke den steuerbaren Speichermangel. Ohne Grenze kann aber eine einzelne Anfrage den ganzen Server in Speichernot bringen. Setze die Grenze deshalb am Container oder per cgroup, nicht mit RLIMIT_AS oder RLIMIT_DATA, die lösen genau den verwundbaren Pfad aus. Auf geteilten Hosts ohne Grenze je Pool ist das ein schlechter Tausch.
  • post_max_size klein halten: laut PHP 8M ohne Uploads, 32M mit Uploads, upload_max_filesize darunter. Grenzen für Größe, Feldanzahl, Teile und Kopfzeilen am Proxy helfen zusätzlich, auch für Anfragen ohne Längenangabe.
  • Einstellungen pro Anfrage aufräumen: veraltete oder ungültige Werte in .htaccess per php_value oder per PHP_VALUE vom Webserver erzeugen vor der Initialisierung Warnungen und sind ein bestätigter Auslöser. error_reporting, display_errors oder @ helfen dagegen nicht.
  • pm.max_requests klein setzen: Je öfter PHP-FPM einen Prozess ersetzt, desto kürzer lebt übrig gebliebener Zustand. Der Wert 1 ist am sichersten und kostet spürbar Leistung.

Prüfe die Werte im laufenden Dienst. Die Kommandozeile liest oft eine andere php.ini, und manche Images laden eigene Dateien nach deiner. Im offiziellen DokuWiki-Image etwa setzt die Datei dokuwiki.ini memory_limit aus einer Umgebungsvariablen und lädt nach custom.ini; dort hilft nur PHP_MEMORYLIMIT=-1 im Container. Für PHP-FPM zeigt php-fpm8.3 -i die Werte, die neue Worker bekommen.

Was du mitnehmen kannst: session.upload_progress.enabled = 0 und bei 8.5 fatal_error_backtraces = 0 kosten fast nichts und sollten auch nach dem Update bleiben. memory_limit = -1 ist eine Abwägung und gehört nach dem Update wieder zurück.

Was die Notlösungen kosten und was nicht hilft

  • Uploads funktionieren nicht mehr: Medien im CMS, Formulare mit Dateianhang und Upload-Komponenten in Frameworks liefern 403. Manche Formulare senden multipart auch ganz ohne Datei, die fallen ebenfalls aus.
  • Ausnahmen bleiben angreifbar: Jede erlaubte Adresse oder Route ist bis zum Update offen. Eine Ausnahme sollte so klein wie möglich sein und nur aus deinem tatsächlichen Datenverkehr abgeleitet werden.
  • Nicht ausreichend: der Wartungsmodus oder ein Login der Anwendung, weil der Angriff vorher greift; das Verstecken von Warnungen mit error_reporting, display_errors oder @; disable_functions, das höchstens den Schaden nach einem Einbruch begrenzt. Ein kleines post_max_size verringert das Risiko, schließt die Lücke aber nicht.
  • Der Proxy schützt nur, was durch ihn läuft: Direkt erreichbare PHP-Server, Admin-Ports und interne Dienste ohne Proxy bleiben offen. Dazu zählen auch Geräte mit PHP-Weboberfläche wie Firewalls und NAS-Systeme.

Das Muster kennt man: frühere „Shell“-Lücken

PHP2Shell reiht sich in eine Serie ein, bei der ein weit verbreitetes Werkzeug Eingaben aus dem Netz verarbeitet, bevor irgendeine Anwendungslogik eingreifen kann. Die Abwehr folgt jedes Mal demselben Dreischritt: Bestand kennen, den Auslöser am Rand abfangen, dann zügig patchen.

  • Shellshock (2014): Bash führte Befehle aus Umgebungsvariablen aus, die ein Webserver per CGI aus Anfrage-Headern setzte. Auch dort lief der Angriff, bevor das eigentliche Skript startete.
  • Heartbleed im Rechenzentrum aufspüren (2014): zeigt, warum ein Inventar über alle Dienste zählt. Wie bei PHP steckte die verwundbare Bibliothek in mehr Diensten, als man auf Anhieb vermutet.
  • ImageTragick (2016): ImageMagick machte hochgeladene Bilddateien zu Befehlen. Die Notlösung war ebenfalls eine Sperre vor dem Parser, damals per policy.xml.
  • Log4Shell (2021): Die Java-Bibliothek Log4j wertete protokollierte Zeichenketten aus und lud Code nach. Der Beitrag fragt außerdem, wer solche Reparaturen an freier Software bezahlt; das von der PHP Group 2024 beauftragte Code-Audit und das im Mai 2026 gegründete PHP Ecosystem Security Team sind laut heise eine Antwort darauf.
  • regreSSHion (2024): eine Wettlaufsituation in OpenSSH vor der Anmeldung. Wie bei PHP2Shell brauchte der Angriff viele Versuche, war aber automatisierbar.

Wenn das Update da ist

  • Paketversion bzw. php -v in jedem Dienst prüfen und PHP-FPM, Apache mit mod_php und dauerhaft laufende PHP-Worker neu starten. Container-Images neu ziehen oder bauen und den Container neu erstellen.
  • Erst dann die Proxy-Sperre entfernen und testen, ob Uploads wieder gehen. memory_limit auf den alten Wert zurücksetzen.
  • session.upload_progress.enabled = 0 und bei 8.5 fatal_error_backtraces = 0 beibehalten, solange keine Anwendung die Funktionen braucht.
  • Auf Spuren prüfen: neue PHP-Dateien in Webverzeichnissen, ein gesetztes auto_prepend_file, neue Cronjobs oder systemd-Units, neue SSH-Schlüssel und neue Admin-Konten in den Anwendungen. Bei Verdacht den Server isolieren, Logs sichern und alle Zugangsdaten auf dem System erneuern.

Zahlen im Überblick

  • PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35 vom 21. Oktober 2026 beheben CVE-2026-109618 und CVE-2026-109619, beide CVSS v4.0 9.2.
  • Für PHP 7.4, 8.0 und 8.1 gibt es nur minimale Quell-Backports des Sicherheitsteams, keine regulären Releases.
  • Ubuntu 24.04 liefert den Fix als php8.3 8.3.6-0ubuntu0.24.04.13; die PHP-Versionsnummer bleibt 8.3.6.
  • fatal_error_backtraces ist in PHP 8.5 ab Werk eingeschaltet und Grundlage des bestätigten Angriffs auf mod_php.
  • session.upload_progress.enabled ist ab Werk eingeschaltet und lässt sich weder per ini_set() noch per .user.ini wirksam abschalten.

Referenzen

Bemerkungen

  • Die erste Fassung entstand am 21. Oktober 2026 vormittags, vor dem Release, und stützte sich auf die Ankündigung und erste Berichte. Diese Fassung vom Abend folgt der Sicherheitsmeldung und den beiden Hinweisen des PHP-Teams; überholte Angaben habe ich ersetzt, nicht stehen lassen.
  • Die Caddy-Regel habe ich an einem produktiven Caddy 2.11 von außen geprüft: multipart-POST 403, normale Seiten 200. Die Ubuntu- und Docker-Angaben stammen aus eigenen Abfragen am Nachmittag des 21. Oktober. Die Apache-Regeln sind von mir nicht getestet, deshalb der Abschnitt zum Testen.

Sicherheitsmeldung des PHP-Teams lesen