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
@@ -46,6 +46,47 @@ final class FormRepository
|
||||
/** A human needs longer than this to fill in a form; anything faster is a script. */
|
||||
public const MIN_SECONDS = 3;
|
||||
public const MAX_SECONDS = 86400;
|
||||
/**
|
||||
* Gespeicherte Anfragen je Formular und insgesamt je Stunde (ADR 116). Darüber hinaus wird
|
||||
* nichts mehr gespeichert und nichts verschickt - eine Flut füllt sonst Postfach und
|
||||
* Datenbank, und die Grenze je Absender hilft nicht gegen viele Absender.
|
||||
*/
|
||||
public const FORM_RATE_LIMIT = 30;
|
||||
public const GLOBAL_RATE_LIMIT = 120;
|
||||
|
||||
/**
|
||||
* Das Siegel eines Formulars: gebunden an das Formular und an den Zweck, damit ein von einer
|
||||
* Seite geholtes Siegel nicht für jedes Formular gilt (ADR 116).
|
||||
*/
|
||||
public static function stamp(string $slug, int $issued, string $secret): string
|
||||
{
|
||||
return hash_hmac('sha256', 'formular|'.$slug.'|'.$issued, $secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wer ein Absender ist, für die Grenze je Stunde. Bei IPv6 zählt das /64-Netz: ein einzelner
|
||||
* Anschluss hat davon Milliarden Adressen, mit der vollen Adresse wäre die Grenze wirkungslos.
|
||||
*/
|
||||
public static function senderKey(?string $ip, string $secret): string
|
||||
{
|
||||
$ip = (string) $ip;
|
||||
if (str_contains($ip, ':') && false !== ($binary = @inet_pton($ip)) && 16 === \strlen($binary)) {
|
||||
$ip = bin2hex(substr($binary, 0, 8)).'::/64';
|
||||
}
|
||||
|
||||
return hash_hmac('sha256', 'absender|'.$ip, $secret);
|
||||
}
|
||||
|
||||
/** Ob für dieses Formular oder insgesamt in der letzten Stunde schon zu viel ankam. */
|
||||
public function flooded(string $formId): bool
|
||||
{
|
||||
$since = gmdate('Y-m-d H:i:s', time() - 3600);
|
||||
if (self::FORM_RATE_LIMIT <= (int) $this->db->fetchOne('SELECT COUNT(*) FROM form_submission WHERE form_id = ? AND created_at >= ?', [$formId, $since])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::GLOBAL_RATE_LIMIT <= (int) $this->db->fetchOne('SELECT COUNT(*) FROM form_submission WHERE created_at >= ?', [$since]);
|
||||
}
|
||||
|
||||
public function __construct(private Connection $db)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user