Skip to content

Security ​

Grenzen des Starters ​

  • Geheimnisse existieren nur in GitHub Secrets und in serverseitigen Konfigurationsdateien, niemals im Repository und niemals in einer PUBLIC_*-Variable.
  • Formulare akzeptieren serverseitig ausschließlich Felder aus einer Allowlist.
  • Fehlermeldungen nach außen sind neutral; Details stehen nur im Server-Log.

Was das Frontend beiträgt ​

Der Schutz gegen Missbrauch liegt im Backend — es ist die einzige Seite, die sich nicht umgehen lässt. Das Frontend liefert die Bausteine dafür und trifft bewusst keine Entscheidung:

  • Honeypot. Ein Feld, das aus dem Layout genommen und aus dem Sichtfeld geschoben wird — ausdrücklich kein display: none. Der gefüllte Zustand wird mitgesendet, statt im Browser abgelehnt zu werden: Sonst stünde die Prüfung im ausgelieferten Bündel zum Nachlesen.
  • Turnstile. Lädt erst bei der ersten Berührung des Formulars. Der Site Key ist öffentlich und gehört ins HTML; der Secret Key liegt ausschließlich im Backend.
  • Übermittlungskennung. Eine Kennung je Vorgang, stabil über einen zweiten Versuch hinweg, damit ein Backend Doppelsubmissions erkennen kann.
  • Feld-Allowlist. Was gesendet wird, entscheidet src/content/forms.ts — nicht das, was zufällig im Formular steht.

Alles Weitere — Origin-Prüfung, Größenlimits, Rate Limiting, Captcha-Verifikation — gehört ins Backend.

Was das Backend entscheidet ​

Der PHP-Handler prüft in dieser Reihenfolge, und die Reihenfolge ist Teil der Zusage:

PrüfungWogegen sie hilft
POST + JSON-Content-TypeEin <form> auf einer fremden Seite kann diesen Typ ohne Preflight nicht senden
GrößenlimitEin Rumpf, der die Verarbeitung belasten soll
Origin-AllowlistEine fremde Seite, die im Browser eines Besuchers sendet
VertragsprüfungAlles, was nicht die vereinbarte Form hat
HoneypotAusfüllende Skripte
Rate LimitingWiederholung — je Adresse und für alle zusammen
DoppelsubmissionDer zweite Klick auf „Absenden"
FeldprüfungWerte, die das Formular gar nicht angeboten hat
CaptchaWas die vorherigen Hürden überstanden hat

Zwei Entscheidungen daran sind erklärungsbedürftig und stehen in ADR 0010: Die Feldprüfung läuft vor dem Captcha, weil ein Turnstile-Token einmal einlösbar ist; und ist Cloudflare nicht erreichbar, wird abgelehnt statt durchgelassen.

Datensparsamkeit ​

  • Das Log enthält Zeitpunkt, Formular, Ausgang, Grund und Kennung — keine Feldinhalte. Die Adresse des Absenders steht nur als Hash darin, gebildet mit einem Wert, den nur diese Installation kennt.
  • Rate-Limit-Zähler tragen denselben Hash als Dateinamen; im Ablageverzeichnis steht keine IP-Adresse.
  • Log und Kennungen werden nach einer konfigurierten Frist gelöscht (retentionDays, Vorgabe 30 Tage).
  • Der Handler speichert keine Anfragen. Was bleibt, ist die E-Mail im Postfach des Kunden.

Geheimnisse ​

WertWo er liegt
Turnstile Site KeyPUBLIC_TURNSTILE_SITE_KEY — öffentlich per Design
Turnstile Secret KeyKT_FORMS_TURNSTILE_SECRET beim Handler
SMTP-Zugangsdatenconfig/config.local.php oder KT_FORMS_SMTP_*
SSH-Schlüssel fürs DeploymentGitHub Secret

config/config.local.php steht in .gitignore, liegt auf dem Server und wird nicht mit dem Build ausgeliefert. Die .htaccess des Handler-Verzeichnisses sperrt sie zusätzlich gegen HTTP-Zugriff — greift sie nicht, ist das SMTP-Passwort lesbar.

Header und Content Security Policy ​

Bei deployment.target: 'mittwald-static' entsteht beim Bauen eine .htaccess mit den Sicherheits-Headern:

HeaderWert
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
X-Frame-OptionsSAMEORIGIN
Permissions-PolicyKamera, Mikrofon, Standort, Zahlung, USB gesperrt
Strict-Transport-Securitynur nach ausdrücklicher Entscheidung
Content-Security-Policyerzeugt, standardmäßig als Report-Only

Die Richtlinie entsteht aus der Projektkonfiguration und dem erzeugten HTML — die Einzelheiten und der Weg von „beobachten" zu „erzwingen" stehen unter Content Security Policy.

Zwei Inline-Skripte sind Absicht

Das Consent-Skript im Kopf muss laufen, bevor irgendetwas anderes lädt (ADR 0008); das Navigationsskript bündelt Astro so klein, dass es im Dokument landet. Beide werden über ihren SHA-256-Hash abgedeckt, nicht über 'unsafe-inline'. Die JSON-LD-Ausgabe braucht keinen Hash: Sie ist ein Datenblock, den der Browser nie ausführt (ADR 0013).

Auf GitHub Pages gibt es diese Header nicht

Dort wird keine .htaccess gelesen, und ein <meta http-equiv> deckt frame-ancestors und report-uri nicht ab. Ein Projekt, dem diese Header wichtig sind, gehört auf mittwald-static.