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
@@ -3098,6 +3098,26 @@ final class CmsTest extends TestCase
|
||||
self::assertStringNotContainsString('Viele Möglichkeiten', $this->request('/')->getContent());
|
||||
}
|
||||
|
||||
public function testALinkedSectionIsRenderedByItsOwnSchemaVersion(): void
|
||||
{
|
||||
// Eine Sektion mit altem Schema-Stand: ihr Text lief nicht durch den Filter für
|
||||
// formatierten Text. In einer neueren Seite darf er deshalb nicht roh erscheinen (ADR 116).
|
||||
$this->partials->create('section', 'alt', 'Alte Sektion', [
|
||||
'schemaVersion' => 11, 'title' => 'Alt', 'description' => 'Alte Sektion.',
|
||||
'blocks' => [['id' => 'alt-section', 'type' => 'section', 'children' => [
|
||||
['id' => 'alt-text', 'type' => 'text', 'props' => ['text' => '<meta http-equiv="refresh" content="0;url=https://fremd.example"><a href="https://fremd.example">Hier</a>']],
|
||||
]]],
|
||||
]);
|
||||
$document = DemoContent::starter('Neu');
|
||||
$document['schemaVersion'] = DocumentValidator::SCHEMA_VERSION;
|
||||
$document['blocks'][] = ['id' => 'ref', 'type' => 'partialref', 'props' => ['partial' => 'alt']];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
|
||||
$html = (string) $this->request('/')->getContent();
|
||||
self::assertStringNotContainsString('<meta http-equiv="refresh"', $html);
|
||||
self::assertStringContainsString('<meta http-equiv=', $html, 'Der Text erscheint als Text.');
|
||||
}
|
||||
|
||||
public function testALinkedSectionChangesOnEveryPageThatEmbedsIt(): void
|
||||
{
|
||||
$sectionId = $this->partials->create('section', 'aufruf', 'Aufruf-Band', [
|
||||
@@ -3470,6 +3490,24 @@ display: flex', '#b1{display:flex;color:red}'];
|
||||
$this->pages->saveDraft('page-1', $document, 0);
|
||||
}
|
||||
|
||||
public function testOwnCssStaysInsideItsBlock(): void
|
||||
{
|
||||
$code = new CustomCode();
|
||||
// Geschwister des Blocks selbst liegen außerhalb - die ganze Seite wäre umgestaltbar (ADR 116).
|
||||
foreach (['& ~ *{display:none}', '&+p{color:red}', '&[a="x]{color:red}', 'color:red /*', 'width:10px*/'] as $css) {
|
||||
try {
|
||||
$code->checkCss($css);
|
||||
self::fail('Angenommen: '.$css);
|
||||
} catch (InvalidDocument) {
|
||||
self::addToAssertionCount(1);
|
||||
}
|
||||
// Was schon gespeichert ist und heute nicht mehr gilt, fällt still weg statt die Seite zu brechen.
|
||||
self::assertSame('', $code->compileCss('b1', $css));
|
||||
}
|
||||
// Innerhalb des Blocks bleiben ~ und + erlaubt.
|
||||
self::assertSame('#b1 .a~.b{color:red}#b1 h3+p{margin:0}', $code->compileCss('b1', '& .a~.b{color:red}& h3+p{margin:0}'));
|
||||
}
|
||||
|
||||
public function testScriptChangesAreComparedAgainstWhatIsStored(): void
|
||||
{
|
||||
$code = new CustomCode();
|
||||
@@ -4614,6 +4652,16 @@ Achern']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTheConsentFormRedirectsOnlyWithinThisSite(): void
|
||||
{
|
||||
$this->askConsent();
|
||||
// Browser entfernen Tabulator und Zeilenumbruch: aus /<Tab>/fremd würde //fremd (ADR 116).
|
||||
foreach (["/\t/fremd.example", "/\n/fremd.example", '//fremd.example', '/\fremd.example', 'https://fremd.example', '/ /fremd.example'] as $back) {
|
||||
self::assertSame('/', $this->request('/einwilligung', 'POST', ['choice' => 'none', 'back' => $back])->headers->get('Location'), json_encode($back));
|
||||
}
|
||||
self::assertSame('/kontakt?a=1#x', $this->request('/einwilligung', 'POST', ['choice' => 'none', 'back' => '/kontakt?a=1#x'])->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function testTheConsentScriptSavesWithoutARedirect(): void
|
||||
{
|
||||
$this->askConsent();
|
||||
|
||||
Reference in new issue
Block a user