# Sicherheit im CMF: Was unter der Haube passiert

> Login-Sperre, CSRF-Schutz, HTML-Filter, Content-Security-Policy, SVG-Bereinigung und Schreibsperre: welche Schutzebenen das CMF mitbringt und was an dir liegt.

Kanonische URL: https://cmf.brosemedien.de/news/sicherheit-im-cmf

---

# Sicherheit im CMF: Was unter der Haube passiert

Die wichtigsten Schutzmaßnahmen sind eingebaut und ab Werk aktiv – ohne Plugins, ohne externe Dienste. Ein paar Punkte bleiben trotzdem Sache des Betreibers.

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 `onclick` und **style-Attribute** fliegen raus
- **Links** nur relativ, `http(s)`, `mailto` oder `tel` – auch verschleierte `javascript:`-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](/news/content-security-policy-erklaert).

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.

- [CMF herunterladen](/download)
- [CSP erklärt](/news/content-security-policy-erklaert)
- [Installationsanleitung](/installationsanleitung)
