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 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-22 06:54:12 +02:00
1 parent d3ee8e3d1e
commit 9d80af3795
7 files changed
+58 -5

No files matched your search

+12
View File
@@ -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“.
+5
View File
@@ -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);
+3 -2
View File
@@ -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')
+32
View File
@@ -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<string, mixed> $auftrag
*
+2 -1
View File
@@ -26,8 +26,9 @@
<td data-label="Fassung"><span class="fassung <?= $i['veraltet'] ? 'f-alt' : 'f-ok' ?>"><?= $v->e($i['fassung']) ?></span><?php if ('git' === $i['art']) { ?> <span class="klein">Git</span><?php } ?></td>
<td data-label="Letzte Meldung"><span class="<?= $i['still'] ? 'still' : '' ?>"><?= $v->e($v->vor($i['alter'])) ?></span></td>
<td data-label="Zustand">
<?php if (null !== $i['php_luecke']) { ?><span class="pille p-fehler" title="<?= $v->e($i['php_luecke']) ?>">PHP <?= $v->e($i['php']) ?> unsicher</span> <?php } ?>
<?php if ($i['wartung']) { ?><span class="pille p-warn">Wartung</span> <?php } ?>
<?php if (null !== $i['auftrag_zustand']) { ?><span class="pille p-<?= $v->e($i['auftrag_zustand']['art']) ?>"><?= $v->e($i['auftrag_zustand']['text']) ?></span><?php } elseif (!$i['wartung']) { ?><span class="klein">–</span><?php } ?>
<?php if (null !== $i['auftrag_zustand']) { ?><span class="pille p-<?= $v->e($i['auftrag_zustand']['art']) ?>"><?= $v->e($i['auftrag_zustand']['text']) ?></span><?php } elseif (!$i['wartung'] && null === $i['php_luecke']) { ?><span class="klein">–</span><?php } ?>
</td>
<td class="aktion">
<?php if ($i['kann_update'] && null !== $neueste && 'wartet' !== ($i['auftrag_zustand']['art'] ?? null)) { ?>
+2 -1
View File
@@ -13,7 +13,8 @@
<?php if (null !== $i['domain_vorher']) { ?><dt>Vorher gemeldet als</dt><dd><?= $v->e($i['domain_vorher']) ?></dd><?php } ?>
<dt>Fassung</dt><dd><span class="fassung <?= $i['veraltet'] ? 'f-alt' : 'f-ok' ?>"><?= $v->e($i['fassung']) ?></span><?= $i['veraltet'] && null !== $neueste ? ' – neueste ist '.$v->e($neueste['version']) : '' ?></dd>
<dt>Aktualisiert über</dt><dd><?= 'git' === $i['art'] ? 'Git-Arbeitskopie' : 'Releasearchiv' ?></dd>
<dt>PHP</dt><dd><?= $v->e($i['php'] ?: '–') ?></dd>
<dt>PHP</dt><dd><?php if (null !== $i['php_luecke']) { ?><span class="pille p-fehler"><?= $v->e($i['php']) ?> unsicher</span>
<span class="klein block"><?= $v->e($i['php_luecke']) ?> Das Update macht der Hoster bzw. das Hosting-Panel, nicht KantSites.</span><?php } else { ?><?= $v->e($i['php'] ?: '–') ?><?php } ?></dd>
<dt>Datenbank</dt><dd><?= $v->e($i['datenbank'] ?: '–') ?></dd>
<dt>Wartungsseite</dt><dd><?= $i['wartung'] ? 'an' : 'aus' ?></dd>
<dt>Letzte Meldung</dt><dd><?= $v->e($v->zeit($i['letzte_meldung'])) ?> (<?= $v->e($v->vor($i['alter'])) ?>)<?= $i['still'] ? ' – <strong>seit über 48 Stunden still</strong>' : '' ?></dd>
+2 -1
View File
@@ -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']],
];
?>
<nav aria-label="Nach Zustand filtern">
@@ -43,7 +44,7 @@ $kacheln = [
<?php if (null !== $filter) { ?><input type="hidden" name="filter" value="<?= $v->e($filter) ?>"><?php } ?>
<label for="q">Suchen</label>
<div class="suche-zeile">
<input id="q" name="q" type="search" value="<?= $v->e($suche) ?>" placeholder="Domain, Kunde, Fassung oder IP" autocomplete="off" aria-describedby="treffer" aria-controls="liste">
<input id="q" name="q" type="search" value="<?= $v->e($suche) ?>" placeholder="Domain, Kunde, Fassung, PHP oder IP" autocomplete="off" aria-describedby="treffer" aria-controls="liste">
<button type="submit" class="knopf">Suchen</button>
<a class="knopf knopf-leise zuruecksetzen" href="/"<?= '' === $suche && null === $filter ? ' hidden' : '' ?>>Zurücksetzen</a>
</div>