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:
1 parent
14e7a3b310
commit
ad7c333215
43 files changed
+810
-53
No files matched your search
@@ -23,6 +23,8 @@ final class InstallFlowTest extends TestCase
|
||||
{
|
||||
private string $root;
|
||||
private string $database;
|
||||
/** @var array<string, string> Cookies wie im Browser: der Nachweis des Installationsschlüssels. */
|
||||
private array $cookies = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@@ -58,6 +60,11 @@ final class InstallFlowTest extends TestCase
|
||||
self::assertSame(302, $response->getStatusCode());
|
||||
self::assertSame('/install', $response->headers->get('Location'));
|
||||
|
||||
// 0b. Ohne Installationsschlüssel öffnet sich kein Schritt (ADR 116).
|
||||
self::assertSame('/install/schluessel', $this->request('/install')->headers->get('Location'));
|
||||
self::assertSame('/install/schluessel', $this->request('/install/datenbank')->headers->get('Location'));
|
||||
$this->unlock();
|
||||
|
||||
// 1. The system check.
|
||||
$check = $this->request('/install');
|
||||
self::assertSame(200, $check->getStatusCode());
|
||||
@@ -92,6 +99,9 @@ final class InstallFlowTest extends TestCase
|
||||
self::assertStringContainsString('Fertig.', $done->getContent());
|
||||
self::assertStringContainsString('Impressum und Datenschutzerklärung ausfüllen', $done->getContent());
|
||||
|
||||
// Der Schlüssel hat seine Arbeit getan und liegt nicht mehr herum.
|
||||
self::assertFileDoesNotExist($this->root.'/var/install.key');
|
||||
|
||||
// 6. The installer is over, and the website answers.
|
||||
self::assertSame(404, $this->request('/install')->getStatusCode());
|
||||
self::assertSame(404, $this->request('/install/konto')->getStatusCode());
|
||||
@@ -105,8 +115,41 @@ final class InstallFlowTest extends TestCase
|
||||
self::assertSame(200, $this->request('/admin/login')->getStatusCode());
|
||||
}
|
||||
|
||||
public function testTheInstallerOpensOnlyWithItsKey(): void
|
||||
{
|
||||
// Wer die frische Installation zuerst aufruft, bekommt nur die Frage nach dem Schlüssel.
|
||||
$page = $this->request('/install/schluessel');
|
||||
self::assertSame(200, $page->getStatusCode());
|
||||
self::assertStringContainsString('Installationsschlüssel', (string) $page->getContent());
|
||||
$key = trim((string) file_get_contents($this->root.'/var/install.key'));
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $key);
|
||||
|
||||
// Ein falscher Schlüssel öffnet nichts und setzt kein Cookie.
|
||||
$wrong = $this->request('/install/schluessel', 'POST', ['schluessel' => str_repeat('0', 32)]);
|
||||
self::assertSame(200, $wrong->getStatusCode());
|
||||
self::assertStringContainsString('passt nicht', (string) $wrong->getContent());
|
||||
self::assertSame([], $wrong->headers->getCookies());
|
||||
// Auch ein selbst gebautes Cookie nicht.
|
||||
$this->cookies = ['kant_install' => hash_hmac('sha256', 'installer', 'geraten')];
|
||||
self::assertSame('/install/schluessel', $this->request('/install')->headers->get('Location'));
|
||||
|
||||
// Der Verweis von install.php trägt den Schlüssel in der Adresse; danach geht es ohne ihn weiter.
|
||||
$this->cookies = [];
|
||||
$open = $this->request('/install/schluessel?schluessel='.$key);
|
||||
self::assertSame('/install', $open->headers->get('Location'));
|
||||
$cookie = $open->headers->getCookies()[0];
|
||||
self::assertSame('kant_install', $cookie->getName());
|
||||
self::assertTrue($cookie->isHttpOnly());
|
||||
self::assertSame('strict', $cookie->getSameSite());
|
||||
self::assertSame('/install', $cookie->getPath());
|
||||
self::assertNotSame($key, $cookie->getValue(), 'Im Cookie steht ein Nachweis, nicht der Schlüssel.');
|
||||
$this->cookies = ['kant_install' => (string) $cookie->getValue()];
|
||||
self::assertSame(200, $this->request('/install')->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAStepWithoutItsRequirementSendsYouBack(): void
|
||||
{
|
||||
$this->unlock();
|
||||
// Somebody who guesses the address of a later step does not get past the missing database.
|
||||
self::assertSame('/install/datenbank', $this->request('/install/migration')->headers->get('Location'));
|
||||
self::assertSame('/install/migration', $this->request('/install/website')->headers->get('Location'));
|
||||
@@ -118,6 +161,15 @@ final class InstallFlowTest extends TestCase
|
||||
self::assertFileDoesNotExist($this->root.'/.env');
|
||||
}
|
||||
|
||||
/** Gibt den Installationsschlüssel ein, wie es der Betreiber tut, und behält das Cookie. */
|
||||
private function unlock(): void
|
||||
{
|
||||
$this->request('/install/schluessel');
|
||||
$response = $this->request('/install/schluessel', 'POST', ['schluessel' => trim((string) file_get_contents($this->root.'/var/install.key'))]);
|
||||
self::assertSame('/install', $response->headers->get('Location'));
|
||||
$this->cookies['kant_install'] = (string) $response->headers->getCookies()[0]->getValue();
|
||||
}
|
||||
|
||||
/** Posts a form with the signature the rendered page carries, the way a browser would. */
|
||||
private function submit(string $url, Response $page, array $fields): Response
|
||||
{
|
||||
@@ -143,7 +195,7 @@ final class InstallFlowTest extends TestCase
|
||||
}
|
||||
$kernel = new InstallTestKernel($this->root);
|
||||
try {
|
||||
$request = Request::create($url, $method, $body);
|
||||
$request = Request::create($url, $method, $body, $this->cookies);
|
||||
$response = $kernel->handle($request);
|
||||
$kernel->terminate($request, $response);
|
||||
|
||||
|
||||
Reference in new issue
Block a user