From 9d80af3795cdf440b962ccf6464d4071f119af5a Mon Sep 17 00:00:00 2001 From: Umut Kantarci Date: Thu, 22 Oct 2026 06:54:12 +0200 Subject: [PATCH] Flag installations with vulnerable PHP in the central registry Mark sites reporting a PHP release without the php2shell fixes (CVE-2026-109618/-109619; patched in 8.2.35, 8.3.36, 8.4.27, 8.5.12) or an end-of-life branch (8.1 and older). The overview gets a counter tile that doubles as a filter, a status pill per row and PHP in the search; the detail page explains that the hoster has to update. Co-Authored-By: Claude Opus 5.5 --- docs/STATUS.md | 12 ++++++++++ scripts/zentrale-check.php | 5 ++++ zentrale/app/Anwendung.php | 5 ++-- zentrale/app/Bestand.php | 32 ++++++++++++++++++++++++++ zentrale/app/vorlagen/_liste.php | 3 ++- zentrale/app/vorlagen/installation.php | 3 ++- zentrale/app/vorlagen/uebersicht.php | 3 ++- 7 files changed, 58 insertions(+), 5 deletions(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index e0795db..a375811 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,17 @@ # Projektstatus +## Zentrale: Warnung bei unsicherem PHP (22.10.2026) + +**Anlass:** Kritische PHP-Lücke „php2shell“ (CVE-2026-109618/-109619, CVSS 9.2, Patches vom 21.10.2026: 8.2.35, 8.3.36, 8.4.27, 8.5.12). Umut: Warnung „bei sitesviewer einbauen, nicht in KantSites selbst“. + +**Erledigt:** Nur `zentrale/`, kein Eingriff ins CMS, keine Schemaänderung. `Bestand::PHP_SICHER` (erste sichere Fassung je Zweig) und `PHP_ALT_BIS` (8.1 und älter ohne Updates); `Bestand::phpLuecke()` liefert den Grund oder null. Unbekannte Angaben und neuere Zweige (8.6-Vorabfassungen) gelten nicht als unsicher. Übersicht: neue Kachel/Filter „PHP mit Sicherheitslücke“, rote Pille „PHP x.y.z unsicher“ in der Spalte Zustand, Suche findet auch die PHP-Fassung. Detailseite: Pille plus Hinweis, dass das Update beim Hoster liegt. Bei der nächsten PHP-Sicherheitsmeldung `PHP_SICHER` nachziehen. + +**Geprüft:** `php scripts/zentrale-check.php` grün (drei neue Prüfungen zur PHP-Regel). Lokal im Browser (Port 8115, drei Demo-Installationen mit 8.4.22, 8.4.27, 8.1.30): Kachel zählt 2, Filter zeigt genau die zwei, Detailseite mit Hinweis. Demo-Datenbank wieder gelöscht. Nicht geprüft: Mobilansicht, Live-Zentrale. + +**Bekannte Grenze:** Distributionen mit zurückportierten Patches (z. B. Debian/Ubuntu) melden weiter die alte Versionsnummer und erscheinen dann fälschlich als unsicher. Installationen vor 1.36.0 melden kein PHP. + +**Nächster Schritt:** Umut lädt `zentrale/app/` und `zentrale/public/` neu hoch (Paket `var/zentrale-upload.zip`, neu gebaut); `config.php` und `var/` auf dem Server bleiben. PHP bei allen Hostings auf 8.4.27 bringen. + ## Symfony-Patchupdate, Release 1.36.1 (14.10.2026) **Anlass:** Monatlicher Sicherheitscheck (`reports/sicherheit-2026-10.md`, nicht in Git): keine Sicherheitslücken, aber Patch-Rückstand bei Symfony. Umut: „dann mach neueste symfony version dann kann ich den updater mal testen“. diff --git a/scripts/zentrale-check.php b/scripts/zentrale-check.php index b1b2d9f..047d03b 100644 --- a/scripts/zentrale-check.php +++ b/scripts/zentrale-check.php @@ -105,5 +105,10 @@ $umzug->melden(); $neu = $pdo->query('SELECT domain, domain_vorher FROM installation')->fetch(); $pruefe('www.kunde-zwei.de' === $neu['domain'] && 'kunde-eins.de' === $neu['domain_vorher'], 'Domainwechsel ist sichtbar'); +// PHP-Fassungen mit bekannter Lücke fallen auf; gepatchte, unbekannte und Vorabfassungen nicht. +$pruefe(null !== Zentrale\Bestand::phpLuecke('8.4.22') && null !== Zentrale\Bestand::phpLuecke('8.1.30'), 'Ungepatchtes und veraltetes PHP gilt als unsicher'); +$pruefe(null === Zentrale\Bestand::phpLuecke('8.4.27') && null === Zentrale\Bestand::phpLuecke('8.6.0RC4') && null === Zentrale\Bestand::phpLuecke(''), 'Gepatchtes, neueres und unbekanntes PHP gilt nicht als unsicher'); +$pruefe(array_key_exists('php_luecke', $bestand->alle(null)[0] ?? []), 'Die Übersicht kennt den PHP-Zustand'); + echo $fehler > 0 ? "\n{$fehler} Prüfung(en) fehlgeschlagen.\n" : "\nAlle Prüfungen bestanden.\n"; exit($fehler > 0 ? 1 : 0); diff --git a/zentrale/app/Anwendung.php b/zentrale/app/Anwendung.php index 73dea7d..e9a5dac 100644 --- a/zentrale/app/Anwendung.php +++ b/zentrale/app/Anwendung.php @@ -123,6 +123,7 @@ final class Anwendung 'veraltet' => static fn (array $i): bool => $i['veraltet'], 'still' => static fn (array $i): bool => $i['still'], 'unbekannt' => static fn (array $i): bool => $i['unbekannt'], + 'php' => static fn (array $i): bool => null !== $i['php_luecke'], ]; $zahlen = ['alle' => count($alle)]; foreach ($filter as $name => $passt) { @@ -141,7 +142,7 @@ final class Anwendung if ('' === $suche) { return true; } - foreach (['domain', 'domain_vorher', 'adresse', 'notiz', 'fassung', 'ip'] as $feld) { + foreach (['domain', 'domain_vorher', 'adresse', 'notiz', 'fassung', 'php', 'ip'] as $feld) { if (null !== $i[$feld] && false !== mb_stripos((string) $i[$feld], $suche)) { return true; } @@ -154,7 +155,7 @@ final class Anwendung $seiten = max(1, (int) ceil(count($treffer) / $proSeite)); $seite = min($seiten, max(1, (int) ($_GET['seite'] ?? 1))); - $namen = ['aktuell' => 'aktuell', 'veraltet' => 'veraltet', 'still' => 'ohne Meldung seit 48 h', 'unbekannt' => 'ohne Kundenzuordnung']; + $namen = ['aktuell' => 'aktuell', 'veraltet' => 'veraltet', 'still' => 'ohne Meldung seit 48 h', 'unbekannt' => 'ohne Kundenzuordnung', 'php' => 'PHP mit Sicherheitslücke']; $von = [] === $treffer ? 0 : ($seite - 1) * $proSeite + 1; $bis = min(count($treffer), $seite * $proSeite); $trefferText = [] === $treffer ? 'Keine Treffer.' : sprintf('%d–%d von %d %s', $von, $bis, count($treffer), 1 === count($treffer) ? 'Installation' : 'Installationen') diff --git a/zentrale/app/Bestand.php b/zentrale/app/Bestand.php index 6eb4259..3bbf1fe 100644 --- a/zentrale/app/Bestand.php +++ b/zentrale/app/Bestand.php @@ -15,6 +15,15 @@ final class Bestand /** Ein abgeholter Auftrag ohne Ergebnis nach so vielen Stunden: keine Rückmeldung. */ public const AUFTRAG_STUNDEN = 2; + /** + * Erste PHP-Fassung je Zweig ohne bekannte kritische Lücke (zuletzt php2shell, + * CVE-2026-109618/-109619, 21.10.2026). Bei der nächsten PHP-Sicherheitsmeldung hier nachziehen. + */ + public const PHP_SICHER = ['8.2' => '8.2.35', '8.3' => '8.3.36', '8.4' => '8.4.27', '8.5' => '8.5.12']; + + /** Bis hierher gibt es keine Sicherheitsupdates mehr. */ + public const PHP_ALT_BIS = '8.1'; + public function __construct(private \PDO $db) { } @@ -126,9 +135,32 @@ final class Bestand 'auftrag_zustand' => $zustand, 'kann_update' => '' === $grund, 'kein_update_grund' => $grund, + 'php_luecke' => self::phpLuecke((string) $zeile['php']), ]; } + /** + * Warum die gemeldete PHP-Fassung als unsicher gilt, oder null. Unbekannte und neuere + * Zweige (etwa 8.6-Vorabfassungen) gelten nicht als unsicher. + */ + public static function phpLuecke(string $php): ?string + { + if (1 !== preg_match('/^(\d+)\.(\d+)\.(\d+)/', $php, $teile)) { + return null; + } + $zweig = $teile[1].'.'.$teile[2]; + $fassung = $zweig.'.'.$teile[3]; + if (version_compare($zweig, self::PHP_ALT_BIS, '<=')) { + return sprintf('PHP %s bekommt keine Sicherheitsupdates mehr – auf PHP %s umstellen.', $zweig, self::PHP_SICHER['8.4']); + } + $sicher = self::PHP_SICHER[$zweig] ?? null; + if (null !== $sicher && version_compare($fassung, $sicher, '<')) { + return sprintf('PHP %s hat eine bekannte kritische Sicherheitslücke – mindestens %s nötig.', $fassung, $sicher); + } + + return null; + } + /** * @param array $auftrag * diff --git a/zentrale/app/vorlagen/_liste.php b/zentrale/app/vorlagen/_liste.php index ef38e3e..c990762 100644 --- a/zentrale/app/vorlagen/_liste.php +++ b/zentrale/app/vorlagen/_liste.php @@ -26,8 +26,9 @@ e($i['fassung']) ?> Git e($v->vor($i['alter'])) ?> + PHP e($i['php']) ?> unsicher Wartung - e($i['auftrag_zustand']['text']) ?>– + e($i['auftrag_zustand']['text']) ?>– diff --git a/zentrale/app/vorlagen/installation.php b/zentrale/app/vorlagen/installation.php index 756fc9d..ede8683 100644 --- a/zentrale/app/vorlagen/installation.php +++ b/zentrale/app/vorlagen/installation.php @@ -13,7 +13,8 @@
Vorher gemeldet als
e($i['domain_vorher']) ?>
Fassung
e($i['fassung']) ?>e($neueste['version']) : '' ?>
Aktualisiert über
-
PHP
e($i['php'] ?: '–') ?>
+
PHP
e($i['php']) ?> unsicher + e($i['php_luecke']) ?> Das Update macht der Hoster bzw. das Hosting-Panel, nicht KantSites.e($i['php'] ?: '–') ?>
Datenbank
e($i['datenbank'] ?: '–') ?>
Wartungsseite
Letzte Meldung
e($v->zeit($i['letzte_meldung'])) ?> (e($v->vor($i['alter'])) ?>)seit über 48 Stunden still' : '' ?>
diff --git a/zentrale/app/vorlagen/uebersicht.php b/zentrale/app/vorlagen/uebersicht.php index 72c9ce6..9bd94a3 100644 --- a/zentrale/app/vorlagen/uebersicht.php +++ b/zentrale/app/vorlagen/uebersicht.php @@ -15,6 +15,7 @@ $kacheln = [ 'veraltet' => ['z-warn', 'veraltet', $zahlen['veraltet']], 'still' => ['z-still', 'ohne Meldung seit 48 h', $zahlen['still']], 'unbekannt' => ['z-fremd', 'ohne Kundenzuordnung', $zahlen['unbekannt']], + 'php' => ['z-fremd', 'PHP mit Sicherheitslücke', $zahlen['php']], ]; ?>