Harden KantSites after the security review

- The installer only opens with a per-installation key that install.php
  creates and shows; the key is deleted with the first account.
- Lock files keep .env, the database, backups and logs out of reach when a
  domain points at the project folder instead of public/.
- Linked sections render by their own schema version, so an old section
  cannot carry unfiltered HTML into a newer page.
- Editors no longer move, trash or change areas and post data of published
  pages; a scheduled publication goes live with the revision that was
  scheduled (migration 20261012000100).
- Form stamps are bound to their form, IPv6 senders count per /64, and a
  flood per form or overall stops storing with a visible message.
- The consent redirect rejects control characters, update targets must look
  like 1.2.3, and own CSS cannot reach outside its block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-12 14:38:25 +02:00
1 parent 14e7a3b310
commit ad7c333215
43 files changed
+810 -53

No files matched your search

+28 -1
View File
@@ -17,6 +17,9 @@
* Eintrag wie ../../.env landet nirgends.
* - Sie arbeitet nur in ein leeres Verzeichnis. Eine bestehende Installation wird nicht
* überschrieben, auch nicht teilweise.
* - Nach dem Entpacken legt sie den Installationsschlüssel an (var/install.key, ADR 116) und
* zeigt ihn nur auf ihrer Abschlussseite. Ohne ihn öffnet sich der Installer nicht - wer die
* frische Installation zuerst aufruft, kann sie so nicht übernehmen.
*/
// Vom Buildskript gesetzt: `php scripts/build-release.php` schreibt Adresse und Prüfsumme des
@@ -176,6 +179,27 @@ function kant_checks(string $target): array
return $checks;
}
/** Legt den Installationsschlüssel an (oder liest einen vorhandenen) und gibt ihn zurück. */
function kant_install_key(string $target): string
{
$file = $target.'/var/install.key';
$key = is_file($file) ? trim((string) file_get_contents($file)) : '';
if (1 === preg_match('/^[a-f0-9]{32}$/D', $key)) {
return $key;
}
if (!is_dir($target.'/var')) {
mkdir($target.'/var', 0775, true);
}
$key = bin2hex(random_bytes(16));
if (false === file_put_contents($file, $key."
")) {
throw new RuntimeException('Der Installationsschlüssel ließ sich nicht schreiben (var/install.key).');
}
@chmod($file, 0600);
return $key;
}
/** Der Ablauf, wenn der Knopf gedrückt wurde. Gibt [Erfolg, Meldung] zurück. */
function kant_run(string $target): array
{
@@ -192,6 +216,7 @@ function kant_run(string $target): array
return [false, 'Die Prüfsumme des heruntergeladenen Archivs stimmt nicht. Erwartet: '.KANT_ARCHIVE_SHA256.', erhalten: '.$hash.'. Es wurde nichts entpackt.'];
}
$files = kant_unpack($archive, $target);
kant_install_key($target);
return [true, 'Entpackt: '.$files.' Dateien.'];
} catch (Throwable $e) {
@@ -259,7 +284,9 @@ header('X-Robots-Tag: noindex, nofollow');
<h1>Die Dateien liegen.</h1>
<p class="lead">Diese Datei hat sich selbst gelöscht. Weiter geht es mit der Einrichtung: Datenbank, Konto, Website.</p>
<p class="notice done"><?= $e($message) ?></p>
<p><a href="install">Zur Einrichtung →</a></p>
<?php $key = kant_install_key($target); ?>
<p>Dein Installationsschlüssel: <code><?= $e($key) ?></code><br><span style="font-size:13px;color:#5d6b62">Nur diese Seite zeigt ihn an. Der Verweis unten trägt ihn schon; liegt er dir nicht mehr vor, steht er in <code>var/install.key</code>.</span></p>
<p><a href="install/schluessel?schluessel=<?= $e($key) ?>" rel="noreferrer">Zur Einrichtung →</a></p>
<p style="font-size:13px;color:#5d6b62">Führt der Verweis ins Leere, zeigt die Domain nicht auf <code>public/</code>. Das muss beim Hoster eingestellt werden – ohne das wären auch <code>.env</code> und <code>var/</code> über das Netz erreichbar.</p>
<?php } else { ?>
<h1>KantSites installieren</h1>