Nichts läuft, wenn du eine Seite aufrufst
Diese Woche hat das PHP-Projekt Fixes für PHP2Shell veröffentlicht, und laut Advisory kann der Angriff „before any application code runs“ zünden1. Was das für PHP bedeutet, habe ich in PHP: Neunundzwanzig Jahre mit offener Tür aufgeschrieben. Dieser Beitrag handelt von der anderen Hälfte der Antwort. Was, wenn gar nichts läuft?
So funktioniert dieser Blog. Als du diese Seite aufgerufen hast, ist kein Programm von mir gestartet, hat keine Datenbank gelesen, keinen Login geprüft und kein HTML gebaut. Eine Datei, die schon existierte, wurde zurückgeschickt. Das ist alles.
So sieht dieser Unterschied aus, gemessen am selben Tag, an einem frisch im Labor installierten Standard-WordPress und an dieser Site, so wie sie jetzt live ist:
| Standard-WordPress 7.1.3 | Diese Site | |
|---|---|---|
| REST-API-Routen | 133 | 0 |
| REST-Routen, die Schreibzugriffe annehmen | 62 | 0 |
| XML-RPC-Methoden | 80 | 0 |
| Dateien, die auf dem Server ausgeführt werden | 1.514 PHP | 0 |
| Dateien, die der ausliefernde Benutzer schreiben kann | 3.785 von 3.785 | keine: es läuft nichts, das schreiben könnte |
| Datenbank | 12 Tabellen, App-Konto mit allen Rechten | keine |
| Login-Seite | wp-login.php, live | keine; der Pfad liefert 404 |
| Ein POST, PUT oder DELETE an die Startseite | PHP läuft und rendert die Seite trotzdem | an der Edge mit 405 abgewiesen; nichts läuft |
Keine dieser Nullen ist eine Einstellung, die ich abgeschaltet habe. Es sind Nullen, weil das, was sie zählen, hier nicht existiert. Eine Login-Seite, die nie gebaut wurde, kannst du nicht angreifen.
Was ein Standard-WordPress ins Internet stellt
Ich wollte das nicht aus dem Gedächtnis behaupten, also habe ich es gemessen. Die offiziellen Container-Images wordpress und mariadb, in Podman auf meinem eigenen Rechner gestartet und an localhost gebunden, installiert ohne irgendeine Ergänzung: keine Plugins über das Mitgelieferte hinaus, das Standard-Theme, ein Administrator. WordPress 7.1.3 auf PHP 8.3.36. Dann habe ich gezählt, was alles antwortet.
Die REST-API ist die größte einzelne Fläche. Fünf Namespaces, 133 Routen, 257 Kombinationen aus Route und Methode, und 62 Routen, die irgendeine Art von Schreibzugriff annehmen. Sie kam mit WordPress 4.7 in den Core, „with REST API endpoints for posts, comments, terms, users, meta, and settings“2. Und abschalten sollst du sie nicht: „You should not disable the REST API; doing so will break WordPress Admin functionality that depends on the API being active.“3 Also bleibt sie an.
| Was standardmäßig offenliegt | Was WordPress’ eigene Dokumentation sagt |
|---|---|
| XML-RPC, 80 Methoden | seit 3.5 standardmäßig an: „Enable XML-RPC by default, remove Writing Settings option“4 |
| die REST-API, 133 Routen | „content that is public on your site is generally publicly accessible via the REST API“5 |
| die Benutzerliste | GET /wp/v2/users liefert in der Standardansicht Anzeigename, Slug, Link und Avatar jedes Autors6 |
wp-login.php und xmlrpc.php | „xmlrpc.php is a frequent brute‑force target“; „Apply rate limits to /wp-login.php and /xmlrpc.php globally.“7 |
wp-cron.php | „WP-Cron works by checking, on every page load, a list of scheduled tasks to see what needs to be run.“8 |
| der Theme- und Plugin-Editor | „This is often the first tool an attacker will use if able to login, since it allows code execution.“9 |
Die Benutzerliste verdient einen zweiten Blick. In meinem Labor hat eine anonyme Anfrage an diesen Endpunkt das eine Administratorkonto zurückgegeben, mit Anzeigename und Slug. Jemand, der sich nie angemeldet hat, erfährt, welche Konten die Site hat, bevor er irgendetwas versucht hat, auf einer frischen Installation, so gewollt. Niemand musste etwas falsch konfigurieren.
Dann die Dateien. Das offizielle Image betreibt Apache als www-data, und www-data gehört das ganze Web-Root. Alle 3.785 Dateien, darunter die 1.514 PHP-Dateien, sind von demselben Benutzer beschreibbar, der den Code ausführt. Jede Datei. Der Code eingeschlossen. wp-config.php, mit dem Datenbankpasswort darin, liegt im Web-Root mit Modus 644. Das Datenbankkonto, das WordPress benutzt, hat ALL PRIVILEGES auf seiner Datenbank.
Das ist das Ein-Prozess-Modell aus dem PHP-Beitrag, live und unverändert, aus dem offiziellen Image. Das ist keine schlecht betriebene Site. Es ist der Ausgangspunkt, von dem jede WordPress-Site startet, bevor das erste Plugin dazukommt.
Und von den Plugins kommt das meiste. Patchstack hat 2025 im WordPress-Ökosystem 11.334 neue Schwachstellen gezählt. „91% of new vulnerabilities were found in plugins, and 9% were found in themes. There were only 6 vulnerabilities reported in the WordPress core.“10 Fast die Hälfte hatte keinen Fix, als sie öffentlich wurde, und bei denen, die Angreifer haben wollten, lag der Median bis zur Massenausnutzung bei fünf Stunden10.
Was diese Site ins Internet stellt
Dieser Blog besteht aus 6.343 Dateien. Hier ist alles, nach Art:
| Dateiart | Anzahl | Was es ist |
|---|---|---|
.html | 3.486 | die Seiten, in fünf Sprachen |
.xml | 1.576 | RSS-Feeds und Sitemaps |
.svg | 1.181 | Diagramme, Cover, Icons |
.png | 46 | Screenshots |
.json | 10 | der Suchindex und die Build-Stempel |
.js | 4 | kleine Skripte, die in deinem Browser laufen, nicht auf dem Server |
.css, Schriften und der Rest | 40 | Styling und an Beiträge angehängte Dateien |
| Auf dem Server ausgeführt | 0 |
Ein paar .py- und .sh-Dateien sind dabei. Das sind Skripte, die an Beiträgen hängen, damit Leser sie herunterladen können, und der Server schickt sie als Text. Nichts auf dem Server führt sie aus. Auf dem Server gibt es schlicht nichts, womit man sie ausführen könnte.
Ausgeliefert wird sie bei Cloudflare als statische Assets, ganz ohne Worker-Skript. Die Deploy-Konfiguration hat keinen Einstiegspunkt, und Cloudflares Dokumentation sagt, was das heißt: „The main key is optional for assets-only Workers.“11 Wenn ein angefragter Pfad zu einer Datei passt, „that file will be served“, ohne dass Worker-Code aufgerufen wird, und „If no Worker script is present, a 404 Not Found response is returned.“12 Kosten tut es auch nichts: „Requests to static assets are free and unlimited.“13
Cloudflare ist eine Wahl, keine Voraussetzung. Die Ausgabe ist ein Ordner mit Dateien, und jeder HTTPS-Server, der eine Datei ausliefern kann, kann sie bedienen: nginx, Caddy, ein Apache, der nur statische Dateien ausliefert, oder ein Object-Storage-Bucket hinter einem CDN. Die Links sind relativ, also läuft derselbe Build unverändert unter einer Domain-Wurzel oder in einem Unterverzeichnis; geprüft, indem die gebaute Seite unter /preview/ ausgeliefert wurde und alle 592 lokalen Verweise auf sechs Seiten aufgelöst wurden. Den Host zu wechseln heißt, einen Ordner zu kopieren. Das einzige Cloudflare-spezifische Stück ist die _headers-Datei; anderswo gehören dieselben Header in die Konfiguration des Servers. Was mitwandert, ist die Eigenschaft, auf die es ankommt. Was die Seite ausliefert, muss nur Dateien senden, also nie deinen Code ausführen.
Die Live-Site antwortet also so:
$ for m in POST PUT DELETE; do curl -s -o /dev/null -w "$m %{http_code}\n" -X $m https://blogs.damiendye.uk/en/; done
POST 405
PUT 405
DELETE 405
$ for p in wp-login.php xmlrpc.php wp-admin/ wp-json/; do curl -s -o /dev/null -w "%{http_code} $p\n" https://blogs.damiendye.uk/$p; done
404 wp-login.php
404 xmlrpc.php
404 wp-admin/
404 wp-json/
Nichts antwortet. Genau so ist es gebaut.
Die Security-Header gehen mit jeder Datei raus, aus einer reinen Textdatei _headers im Repository, die Cloudflare beim Deploy liest14. Die Content Security Policy bindet jedes Skript, jeden Style, jedes Bild, jede Schrift und jede Verbindung an diesen einen Origin. Bei der Lücke darin bin ich ehrlich: script-src und style-src tragen noch 'unsafe-inline', weil das Theme ein paar kleine Skripte direkt in die Seite schreibt. Das ist die offene Aufgabe an dieser Datei, und sie steht genau so im Repository.
Und Veröffentlichen braucht keine Tür. Bei WordPress ist Veröffentlichen ein Schreibzugriff über HTTP: Editor, REST-API und XML-RPC gibt es, weil Beiträge so auf den Server kommen, und jeder davon ist ein Weg hinein. Hier schreibe ich einen Beitrag als Textdatei und pushe ihn in ein Git-Repository, und der Build lädt fertige Dateien über die Deploy-API von Cloudflare hoch. Nichts, was die Seite ausliefert, nimmt einen Schreibzugriff aus dem Web an, weil nichts einen braucht. Das Repository ist außerdem privat. Frag GitHub ohne meine Zugangsdaten danach, und die Antwort ist ein 404. Quellcode, Entwürfe und Historie liegen also auch nicht im Internet.
Angriffsfläche ist eine Zahl, kein Gefühl
OWASP definiert die Angriffsfläche als „the sum of all paths for data/commands into and out of the application, and the code that protects these paths“15 und sagt, man verkleinert sie, indem man „turning off features and interfaces that aren’t being used“15. Das britische National Cyber Security Centre sagt es direkter: „If would-be attackers can’t reach an interface, they can’t attack it.“16 Sein Cyber-Essentials-Programm macht daraus eine Anforderung: „remove or disable unnecessary software (including applications, system utilities and network services)“17.
Also zähl die Wege, nach dem, was sie sind, nicht danach, wie viele es sind:
| Weg hinein oder hinaus | WordPress | Statische Site |
|---|---|---|
| Login und Passwortraten | wp-login.php, XML-RPC | keiner |
| Admin-Oberfläche | /wp-admin/, mit Code-Editor | keine |
| programmierbare API | REST und XML-RPC, mit Schreibzugriff | keine |
| serverseitiger Code pro Anfrage | Core, Theme, jedes Plugin | keiner |
| Datenbank | ja, von allem oben erreichbar | keine |
| beschreibbarer Speicher auf dem Server | Uploads, und im Standard-Image alles | keiner |
| von Besuchern ausgelöste geplante Jobs | wp-cron.php beim Seitenaufruf | keine |
| Fremdcode, den du gepatcht halten musst | jedes Plugin und Theme, auf dem Server | die Build-Werkzeuge, abseits des Servers |
| was eine gestohlene Session einem Angreifer bringt | die Site | nichts, wo man sich anmelden könnte |
Neun Zeilen. Keine davon führt Code auf dem Server aus. Jedes Mal aus demselben Grund. Die Arbeit war erledigt, bevor irgendjemand eine Seite angefragt hat.
Die vorletzte Zeile ist der ehrliche Haken, und um den geht es im Rest dieses Beitrags. Auch eine statische Site führt Code aus. Nur woanders, einmal, vor dem Veröffentlichen.
Wie Hugo den Build sauber hält
Hugo ist „a static site generator written in Go“18, das Programm, das baut, ist also selbst speichersicher und kompiliert, und nach dem PHP-Beitrag zählt das. Seine Sicherheitsseite zieht die Linie genau dort, wo dieser Beitrag sie zieht: Die Ausgabe „runs directly in the browser“, aber „during development and site building, the hugo executable itself is the runtime environment.“19 Und zum Ausliefern ganz klar: „We have no online production server“19. Gut.
Innerhalb des Builds ist standardmäßig alles verriegelt. „Hugo’s built-in security policy, which restricts access to os/exec, remote communication, and similar operations, is configured via allowlists. By default, access is restricted.“20
| Hugos Standard | Was er verhindert |
|---|---|
| „No arbitrary commands“19 | ein Template kann keine Shell auf dem Rechner aufrufen |
exec.allow begrenzt auf Sass, Go, Git, Node und PostCSS20 | nur benannte Werkzeuge, mit festen Flags |
funcs.getenv begrenzt auf ^HUGO_ und ^CI$20 | Templates können deine Geheimnisse nicht aus der Umgebung lesen |
| Remote-Abrufe verweigern IP-Adressen und localhost als Host20 | ein Template kann nicht in dein eigenes Netz greifen |
Module gegen go.sum geprüft19 | eine manipulierte Abhängigkeit lässt den Build scheitern |
Diese Site weitet eine davon mit Absicht auf. Sie nimmt die CI-Commit-Variablen in funcs.getenv auf, damit der Build-Stempel unter /en/version.json festhalten kann, welcher Commit die Live-Site gebaut hat. Das ist eine getroffene und aufgeschriebene Entscheidung, und genau dafür ist eine Allowlist da. Insofern ist es kein Loch. Es ist eine Entscheidung.
Wohin das Risiko wandert: in den Build
Nimm den Server weg, und das Risiko verschwindet nicht. Es wandert zu dem, was die Dateien baut. Das SLSA-Framework benennt die beiden Bedrohungen genau: eine „build integrity threat“, bei der ein Angreifer „behavior to an artifact without changing its source code“ einschleust, und eine „dependency threat“, durch „compromising some other artifact that the former depends on at build time“21.
Diese Site steht in der mittleren Zeile, und ich werde nicht so tun, als wäre es anders. Sie wird bei jedem Push ins Repository in Cloudflares Build-Container gebaut. Dieser Container ist nicht der Server, der dir antwortet, und er ist weg, sobald der Build fertig ist. Aber für die Dauer des Builds wird ihm alles anvertraut, was am Ende auf der Site landet.
Und der Build hatte zwei Lücken, beide habe ich beim Schreiben dieses Beitrags gefunden. Eine ist behoben. Eine ist eine Entscheidung.
| Lücke | Warum sie zählt | Status |
|---|---|---|
| dem Hugo-Binary wurde blind vertraut: der eigene Download des Skripts hatte keine Prüfsummenkontrolle, und in der CI übersprang es den Download sowieso und nahm die Kopie, die Cloudflares Build-Image installiert hatte | ein ausgetauschtes Binary würde bauen, was es will | behoben, 23. Oktober 2026: der Build lädt Hugo immer selbst herunter, prüft es gegen die veröffentlichte SHA-256 des Releases22 und verweigert den Build bei Abweichung |
das Theme folgt bei jedem Build seinem Upstream-Branch master | eine Änderung upstream kann die Live-Site ohne einen Commit hier verändern | offen, mit Absicht; der Fix ist, es auf einen Commit festzunageln oder in ein Hugo-Modul zu verlegen, das go.sum abdeckt19 |
Die erste ist von der Sorte, die du nur findest, wenn du dein eigenes Build-Skript Zeile für Zeile liest. Das Skript hat die Version geprüft. Aber ein Versionsstring ist etwas, das ein Binary über sich selbst sagt. Über die Bytes beweist er nichts. Jetzt werden die Bytes geprüft:
Fetching Hugo Extended 0.167.0...
Hugo verified: sha256 0163f5c3deddac1f494a1629ddc40c65d18de9d5794facd98f7f96ac2c7d8957
Und eine Kopie desselben Tarballs mit einem angehängten Byte wird abgelehnt:
Refusing to build: hugo_extended_0.167.0_linux-amd64.tar.gz does not match the release checksum
expected 0163f5c3deddac1f494a1629ddc40c65d18de9d5794facd98f7f96ac2c7d8957
got 92682d765d9d34bc53d6f818d56e598d34b5b8267a1cc3b9971da0f53c792251
Das Theme ist ein bewusster Tausch, Aktualität gegen Reproduzierbarkeit, und es ist als solcher aufgeschrieben. Aber es ist genau die Dependency-Bedrohung, die SLSA beschreibt, und ehrlich ist, das auch zu sagen. Keine der beiden Lücken hat die Live-Site je einem Besucher ausgesetzt. Beide waren Vertrauen in den Build, und ich zähle sie lieber selbst auf, als dass ein Leser sie findet.
Selbst die Prüfsumme hat eine Grenze. Sie beweist, dass die heruntergeladene Datei die Datei auf der Release-Seite ist. Sie beweist nicht, wer sie dort abgelegt hat: Unter den Dateien des Releases gibt es keine Signaturen22. Das ist Integrität, nicht Authentizität, und mehr bietet das Projekt heute nicht.
Offline bauen, einmal
Die strengste Variante holt den Build von den Rechnern anderer Leute. Bau auf einem Rechner, der dir gehört und den du kontrollierst, prüf, was du gebaut hast, und lad nur die fertigen Dateien hoch. Nichts im Internet führt jemals deinen Build aus, und der Hoster sieht nie etwas anderes als die Ausgabe.
| Schritt | Warum |
|---|---|
| das exakte Hugo-Release installieren und gegen die veröffentlichte SHA-256 prüfen | du weißt, welches Binary deine Site gebaut hat |
Theme und Module auf Commits festnageln und go.sum committen | der Build ändert sich nur, wenn du ihn änderst |
in ein sauberes Ausgabeverzeichnis bauen, ohne --buildDrafts | was du veröffentlichst, ist das, was du veröffentlichen wolltest |
| vor dem Hochladen in der Ausgabe nach allem Ausführbaren suchen | eine statische Site sollte nichts enthalten, was der Server ausführen würde |
| nur das Ausgabeverzeichnis hochladen, mit einem reinen Deploy-Zugang | der Hoster hat nie deinen Quellcode, deine Werkzeuge oder deine Historie |
| von einem Hoster ausliefern, der für statische Dateien keinen Code ausführt | beim Ausliefern bleibt nichts zu patchen |
Das sind ein paar Befehle, kein Projekt. Und es ändert, wo ein Angreifer sein muss, um dir zu schaden. Um eine WordPress-Site zu verändern, braucht er eine Anfrage an den Server. Um diese Art Site zu verändern, braucht er deinen Build-Rechner. Das ist ein viel kleineres Ziel, und es ist eins, dem du den Stecker ziehen kannst.
Zehntausend Seiten, gemessen
Die Angriffsfläche ist ein Preis dafür, bei jeder Anfrage Code laufen zu lassen. Der Strom ist ein anderer, der Plattenplatz noch einer. Also habe ich beide unter dieselbe Last gesetzt: 10.000 Anfragen an die Startseite, zehn gleichzeitig, vom selben Laptop, über einfaches HTTP auf localhost, damit keine Seite für TLS bezahlt. Auf der einen Seite das Standard-WordPress aus dem Labor. Auf der anderen die echten gebauten Dateien dieser Seite, ausgeliefert von nginx in einem Container, denn an die Edge von Cloudflare kann ich kein Messgerät hängen. Die CPU-Zeit jedes Containers habe ich vor und nach jedem Lauf aus seiner cgroup gelesen, drei Läufe pro Seite.
| 10.000 Seitenanfragen | Standard-WordPress 7.1.3 | Diese Seite auf nginx |
|---|---|---|
| CPU-Zeit, drei Läufe | 409 s, 530 s, 575 s | 0,15 s, 0,18 s, 0,19 s |
| CPU-Zeit pro Seite | 41 bis 57 ms | 15 bis 19 µs |
| Seiten pro Sekunde | 168 bis 236 | 23.679 bis 28.044 |
| Speicher unter Last | 302 bis 310 MiB, plus 145 MiB für die Datenbank | 15 MiB |
| Fehlgeschlagene Anfragen | 0 | 0 |
Das ist das Zwei- bis Dreieinhalbtausendfache an Prozessorzeit für dieselben zehntausend Seiten. Jede dieser CPU-Sekunden kam aus der Steckdose und wurde zu Wärme. Die statische Seite hat alle 10.000 in ungefähr der Prozessorzeit ausgeliefert, die WordPress für drei oder vier seiner Seiten brauchte.
Die Platte erzählt dieselbe Geschichte:
| Auf der Platte und im Speicher | Standard-WordPress | Diese Seite |
|---|---|---|
| Container-Images zum Betrieb | 803 MB WordPress, 334 MB MariaDB | 166 MB nginx, oder gar nichts bei Cloudflare |
| Die Seite selbst | 115 MB Webroot, für eine leere Seite | 114 MB, fünf Sprachen, jeder Beitrag und jedes Diagramm |
| Datenbank | 163 MB Dateien mit 0,72 MB Daten | keine |
| Speicher im Leerlauf | 170 MiB, plus 139 MiB für die Datenbank | 13 MiB |
Die WordPress-Zahlen sind eine frische Installation mit einem Beispielbeitrag. Die statischen Zahlen sind dieses ganze Blog.
Fairerweise: Ein Plugin für einen vollständigen Seiten-Cache würde einen Großteil des CPU-Abstands schließen. Aber schau dir an, was diese Lösung ist. Es ist eine statische Kopie jeder Seite, vor das PHP gestellt, damit das PHP nicht läuft. Das Heilmittel ist genau das, wofür dieser Beitrag argumentiert, seitlich angeschraubt an das, wogegen er argumentiert, mit Login-Seite, API und Datenbank, die alle noch dahinter stehen.
Was du aufgibst
Die ehrliche Liste, denn eine statische Site ist nicht für alles das richtige Werkzeug:
| Du gibst auf | Was diese Site stattdessen macht |
|---|---|
| Kommentare | keine; Leser können mailen |
| serverseitige Suche | ein JSON-Index, mit der Site gebaut und in deinem Browser durchsucht |
| Formulare, die an den Server schicken | keine |
| Logins und Mitgliederbereiche | keine; hier ist alles öffentlich |
| Bearbeiten im Browser | Beiträge sind Textdateien in einem Git-Repository |
| sofortiges Veröffentlichen ohne Build | ein Build dauert ein oder zwei Minuten |
Wenn du einen Shop, einen Mitgliederbereich oder Benutzerkonten brauchst, brauchst du etwas, das auf einem Server läuft, und dieser Beitrag erzählt dir nichts anderes. Was er dir erzählt: Ein Blog, eine Firmenbroschüre im Netz oder eine Dokumentation braucht nichts davon. Die meisten WordPress-Sites sind eins von diesen dreien. Sei ehrlich, welches deine ist.
Einmal bauen und tot ausliefern
Was mich stört, ist, wie viel Risiko ohne jeden Grund mitgeschleppt wird. Eine Firmenseite, die sich zweimal im Jahr ändert, und dahinter laufen eine Datenbank, eine Login-Seite, eine API mit 62 beschreibbaren Routen und ein Job-Runner, der jedes Mal feuert, wenn jemand vorbeischaut. Alles dafür da, dasselbe HTML zu erzeugen wie gestern. Jeden Tag.
Das ist keine technische Entscheidung. Es ist eine Gewohnheit, und es ist eine Gewohnheit, die jemand in Rechnung stellt. Hosting, Plugins, Sicherheits-Add-ons, das Aufräumen, wenn es schiefgeht. Der Preis ist ein Managed-WordPress-Tarif. Was es kostet, ist jeder dieser Wege, offen gehalten, gepatcht und überwacht, für Inhalte, die ein Ordner voller Dateien hätten sein können.
Der Maßstab, an dem ich diese Site messe, ist einfach. Nichts läuft, was nicht laufen muss. Jede Lücke steht da, wo der Nächste sie findet, auch die beiden im Build, und eine davon ist schon geschlossen. Mit Cleverness hat das nichts zu tun. Ein Standard ist das, was du einhältst, wenn keiner nachsieht, und bei einem Blog sieht keiner nach, bis zu dem Tag, an dem er den Code von jemand anderem ausliefert.
Also bau es einmal, prüf, was du gebaut hast, und liefer es tot aus. Was dann noch anzugreifen bleibt, steht auf deinem Rechner, und da gehört es hin.
PHP — News archive 2026, 21st October — „We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs“. ↩︎
WordPress 4.7 release notes — „WordPress 4.7 comes with REST API endpoints for posts, comments, terms, users, meta, and settings.“ ↩︎
WordPress REST API Handbook — Frequently Asked Questions — „You should not disable the REST API; doing so will break WordPress Admin functionality that depends on the API being active.“ ↩︎
WordPress 3.5 release notes — „Enable XML-RPC by default, remove Writing Settings option“. ↩︎
WordPress REST API Handbook — „content that is public on your site is generally publicly accessible via the REST API“. ↩︎
WordPress REST API reference — Users —
GET /wp/v2/users, Standardkontext „view“; name, slug, link, avatar_urls im View-Kontext. ↩︎WordPress — Brute force attacks — „xmlrpc.php is a frequent brute‑force target“; „Apply rate limits to /wp-login.php and /xmlrpc.php globally.“ ↩︎
WordPress Plugin Handbook — Cron — „WP-Cron works by checking, on every page load, a list of scheduled tasks to see what needs to be run.“ ↩︎
WordPress — Hardening WordPress — der Dashboard-Editor „is often the first tool an attacker will use if able to login, since it allows code execution.“ ↩︎
Patchstack — State of WordPress Security in 2026 — Daten für 2025: 11.334 neue Schwachstellen; 91 % Plugins, 9 % Themes, 6 im Core; 46 % nicht rechtzeitig zur Offenlegung behoben; Median 5 Stunden bis zur Massenausnutzung. ↩︎ ↩︎
Cloudflare — Wrangler configuration — „The main key is optional for assets-only Workers.“ ↩︎
Cloudflare — Workers static assets — eine passende Datei „will be served“, ohne Worker-Code aufzurufen; „If no Worker script is present, a 404 Not Found response is returned.“ ↩︎
Cloudflare — Static assets billing and limitations — „Requests to static assets are free and unlimited.“ ↩︎
Cloudflare — Static assets headers — Header, gesetzt über „a plain text file called _headers“. ↩︎
OWASP — Attack Surface Analysis Cheat Sheet — „the sum of all paths for data/commands into and out of the application, and the code that protects these paths“; verkleinern durch „turning off features and interfaces that aren’t being used“. ↩︎ ↩︎
NCSC — Making compromise difficult, 2.2 Reduce attack surface — „If would-be attackers can’t reach an interface, they can’t attack it.“ ↩︎
NCSC — Cyber Essentials: Requirements for IT infrastructure v3.2 — „remove or disable unnecessary software (including applications, system utilities and network services)“. ↩︎
Hugo — Introduction — „Hugo is a static site generator written in Go, optimized for speed and designed for flexibility.“ ↩︎
Hugo — Security model — „during development and site building, the hugo executable itself is the runtime environment“; „We have no online production server“; „No arbitrary commands“; eine abweichende go.sum-Prüfsumme lässt den Build scheitern. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Hugo — Configure security — „By default, access is restricted“; Standard-Allowlists für exec, getenv und http. ↩︎ ↩︎ ↩︎ ↩︎
SLSA v1.2 — Threats — Build-Integritätsbedrohung: „behavior to an artifact without changing its source code“; Dependency-Bedrohung: „compromising some other artifact that the former depends on at build time“. ↩︎
Hugo v0.167.0 release — 36 Assets, darunter
hugo_0.167.0_checksums.txt(SHA-256); keine Signatur-, SBOM- oder Attestation-Dateien. ↩︎ ↩︎