Sicherheit im CMF: Was unter der Haube passiert
Sicherheit ist keine Zutat, die man nachträglich einstreut. Das CMF bringt seine Schutzebenen im Code mit – von der Passwortspeicherung über die Content-Security-Policy bis zum Update. Dieser Beitrag zeigt, was wofür zuständig ist, und am Ende ehrlich, was an dir liegt.
Zugang: Passwörter, Tokens, Login
Passwörter
Passwörter werden mit bcrypt gehasht; in users.json steht nie ein Klartext. Geprüft wird mit password_verify(), der Benutzername zeitkonstant mit hash_equals().
Das Download-Paket startet mit admin / admin – seit Version 1.17 lässt das CMF danach nichts anderes zu, bis ein eigenes Passwort mit mindestens acht Zeichen gesetzt ist.
API-Tokens
API-Tokens werden als SHA-256-Hash gespeichert. Den Klartext siehst du genau einmal, beim Anlegen des Zugangs – danach existiert er nirgends im System.
Wichtig: Ein API-Zugang hat volle Rechte. Behandle den Token wie ein Admin-Passwort.
Login-Sperre
Nach jedem Fehlversuch wartet die IP-Adresse länger – die Wartezeit verfünffacht sich: 5 Sekunden, 25, 125 und so weiter, höchstens eine Stunde. IPv6-Adressen zählen je /64-Block, damit ein Adresswechsel die Sperre nicht umgeht.
Parallel abgeschickte Versuche sehen die Sperre sofort, und ein unbekannter Benutzername antwortet genauso schnell wie ein falsches Passwort. Das alles läuft dateibasiert, ohne Redis oder Datenbank.
Im Browser: Sitzung und Ausgabe
CSRF-Schutz
Jede schreibende Aktion im Admin – auch Neue Seite, Neuer Beitrag und Abmelden – verlangt einen Token, der zur Sitzung gehört. Ohne gültigen Token wird sie abgelehnt.
So kann eine fremde Website keine Aktionen im Namen des eingeloggten Benutzers auslösen.
Sitzungs-Cookie
Das Sitzungs-Cookie ist HttpOnly (für JavaScript unsichtbar), bei HTTPS Secure und SameSite=Lax. Nach dem Login bekommt die Sitzung eine neue ID.
Öffentliche Seiten und Formulare setzen gar kein Cookie.
HTML-Filter
Text-Blöcke laufen beim Ausgeben durch einen Filter:
- 17 erlaubte Tags – alles andere wird entfernt
- Event-Handler wie
onclickund style-Attribute fliegen raus - Links nur relativ,
http(s),mailtoodertel– auch verschleiertejavascript:-Adressen werden neutralisiert
Der html-Block wird bewusst nicht gefiltert – er ist für vertrauenswürdige Autoren gedacht.
Content-Security-Policy und Security-Header
Jede Antwort bringt eine Content-Security-Policy mit. Sie legt fest, woher der Browser Skripte, Stylesheets, Schriften und Verbindungen laden darf – ab Werk nur von der eigenen Domain. Brauchst du ein externes Widget, erweiterst du die Regeln gezielt in der site.json, ohne die Basis aufzuweichen. Wie das geht, erklärt der Beitrag Content-Security-Policy erklärt.
Dazu setzt die mitgelieferte .htaccess die üblichen Security-Header: X-Frame-Options gegen das Einbetten in fremde Seiten, X-Content-Type-Options: nosniff, eine Referrer-Policy und eine Permissions-Policy, die Kamera, Mikrofon und Standort abschaltet. Du musst dafür nichts einrichten.
Dateien, Uploads und Schreiben
Ordnerstruktur
Systemcode (app/), Konfiguration (config/) und Inhalte (content/) liegen außerhalb des öffentlichen Verzeichnisses – per Web erreichbar ist nur public/. Zeigt der DocumentRoot versehentlich auf den Projektstamm, sperren eigene .htaccess-Dateien config/ und content/ trotzdem.
Öffentlich sind nur die erzeugten Dateien wie Sitemap, Feed, llms.txt und search-index.json – letztere absichtlich, sie enthält nur veröffentlichte Inhalte.
Uploads
Hochgeladen werden nur erlaubte Dateitypen, und der tatsächliche Inhalt muss zur Endung passen. Jeder Dateiname bekommt einen Zufallsanhang.
SVGs werden per XML-Parser bereinigt – Skripte, Event-Handler und fremde Verweise fliegen raus; was sich nicht sicher bereinigen lässt, wird abgelehnt. In media/ und files/ führt der Server keine Skripte aus, auch nicht getarnt als bild.php.jpg.
Schreiben ohne Datenverlust
JSON-Dateien werden nie direkt überschrieben: erst eine temporäre Datei, dann per rename() atomar ersetzen. Bricht der Prozess ab, bleibt die alte Datei intakt.
Seit Version 1.17 gibt es außerdem eine Schreibsperre: Speichervorgänge aus Admin, API, Formularen und Login laufen nacheinander statt durcheinander – keine Änderung geht still verloren.
Formulare und Updates
Spam-Schutz und Drossel
Der Formular-Block wehrt Bots mit einem versteckten Honeypot-Feld und einer signierten Zeitfalle ab – ganz ohne Cookie. Dazu kommt eine Drossel: höchstens 10 Einsendungen je IP-Adresse pro Stunde, Benachrichtigungen an dich höchstens 100 pro Stunde, Bestätigungsmails an Absender ebenfalls begrenzt.
E-Mail-Kopfzeilen sind gegen Header-Injection geschützt, der CSV-Export gegen Formel-Injection.
Abgesicherte Updates
Das System-Update lädt das Paket nur per HTTPS vom Update-Server, folgt keinen Weiterleitungen und akzeptiert nur Pfade auf diesem Server. Beim Entpacken wird geprüft, dass keine Datei aus dem Zielordner ausbricht.
Vor dem Einspielen entsteht ein Backup, ein Rollback auf die vorherige Version ist jederzeit möglich. Fehlermeldungen von PHP erscheinen nicht im Browser.
Sicherheit ohne Datenbank
Keine Datenbank bedeutet auch: keine datenbankspezifischen Angriffswege.
- Keine SQL-Injection – es gibt kein SQL
- Kein Datenbank-Passwort, das irgendwo herumliegen und durchsickern kann
- Kein Datenbankserver, der erreichbar sein muss
- Backups sind Dateikopien – der ZIP-Export im Admin genügt
Was an dir liegt
Pflicht
- DocumentRoot auf public/ setzen
- HTTPS verwenden
- PHP und Webserver aktuell halten
- CMF-Updates einspielen (Einstellungen → System-Update)
- Auf nginx die Schutzregeln selbst nachbauen – die .htaccess greift dort nicht
Empfohlen
- API-Zugänge nur an vertrauenswürdige Systeme geben
- Für jede Automatisierung einen eigenen Zugang anlegen
- Token erneuern: neuen Zugang anlegen, alten löschen
- Regelmäßig sichern per ZIP-Export
Optional
- Admin-Bereich per IP einschränken
- Rate-Limiting auf Webserver-Ebene
- Monitoring für Dateiänderungen
- html-Blöcke nur von Menschen oder Agenten pflegen lassen, denen du vertraust
Sicherheit ist kein Feature das man einschaltet – sie ist das Ergebnis vieler kleiner Entscheidungen in die gleiche Richtung.