<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Beiträge — szelcsanyi consulting</title><description>Notizen aus der Praxis: verlässliche Systeme bauen, langsame Queries lesen und beraten, sodass ein Team danach stärker dasteht.</description><link>https://szelcsanyi.net</link><language>de</language><copyright>© 2026 Boris Szelcsányi</copyright><item><title>Durch ein Google-Suchergebnis, nicht durch das Monitoring — Analyse eines WordPress-Zero-Days</title><link>https://szelcsanyi.net/de/blog/wordpress-zero-day-post-mortem</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/wordpress-zero-day-post-mortem</guid><description>Eine Website war zehn Tage lang über eine WordPress-Sicherheitslücke vollständig kompromittiert. Die Betreiber trifft keine Schuld, erkannt hat es keines der laufenden Systeme — und nachweisen ließ sich das Einfallstor nur über zwei Datenbankzeilen, die der Angreifer nicht löschen konnte.</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>*Dies ist eine anonymisierte Aufarbeitung eines realen Vorfalls, den ich bearbeitet habe. Kunde, Domains, IP-Adressen, Kontonamen und Dateipfade wurden entfernt oder verallgemeinert — CVEs, Versionsnummern, Zeiträume und Zahlen sind unverändert, denn daraus lässt sich etwas lernen.*

Aufgefallen ist die Website nicht durch einen Nutzer, einen Alarm oder ein Monitoring-System. Aufgefallen ist sie durch ein Google-Suchergebnis.

Jemand suchte nach dem Unternehmen und fand einen Treffer, der unter der Domain des Kunden ein türkisches Online-Casino bewarb. Ein Klick darauf zeigte eine völlig normale Startseite. Genau diese Diskrepanz — Spam im Index, saubere Seite im Browser — ist die Signatur von **Cloaking**: Inhalte, die nur an Suchmaschinen-Crawler ausgeliefert und vor menschlichen Besuchern verborgen werden. Sie bedeutet zugleich, dass der Fehler lange genug bestand, damit Google ihn crawlen, indexieren und ranken konnte.

Als das endlich jemandem auffiel, saß der Angreifer bereits **zehn Tage und acht Stunden** im System.

## Niemand hat etwas falsch gemacht

Das gehört an den Anfang, denn der Reflex nach einer WordPress-Kompromittierung ist die Suche nach dem unachtsamen Menschen.

Den gab es hier nicht. Kein schwaches Passwort. Kein Phishing. Keine wiederverwendeten Admin-Zugangsdaten, kein bösartiges Plugin, das jemand wider besseres Wissen installiert hätte, keine Agentur, die eine Backup-Datei im Webroot liegen ließ. Der Exploit **benötigte überhaupt keine Authentifizierung**. Jeder im Internet, der die Website erreichen konnte, konnte ihn ausführen — und der Proof-of-Concept war öffentlich.

Die Website lief mit WordPress 6.9.4. Behoben war sie in 6.9.5 — veröffentlicht **46 Tage zuvor**.

Das ist die ganze Geschichte: ein Core-Zero-Day, ein öffentlicher Exploit und ein Patch, der existierte, aber nicht eingespielt war. Alles Weitere in diesem Beitrag ist Folge davon.

## Wie der Angriff ablief

Der Exploit — öffentlich als *wp2shell* im Umlauf — verkettet zwei am selben Tag offengelegte WordPress-Core-Schwachstellen. Keine davon führt für sich zu Codeausführung. Zusammen bringen sie eine anonyme HTTP-Anfrage bis zum Administrator.

Unangenehm daran ist: **jeder Schritt lässt WordPress die schädliche Aktion selbst ausführen**. Bis ganz zum Schluss läuft kein fremder Code. Die Artefakte sehen aus wie gewöhnliche Anwendungsausgaben — genau deshalb hat nichts angeschlagen.

**Schritt 1 — Route-Confusion im REST-Batch-Endpunkt ([CVE-2026-63030](https://www.cve.org/CVERecord?id=CVE-2026-63030)).** Der Batch-Endpunkt bündelt mehrere API-Aufrufe zu einer Anfrage und verwaltet sie in drei parallelen Arrays: Anfragen, Handler, Validierungsergebnisse. Eine gezielt fehlerhaft gebaute Teilanfrage wird dem Validierungs-Array hinzugefügt, im Handler-Array aber übersprungen. Die Arrays verschieben sich gegeneinander, und eine Anfrage, die die Validierung bestanden hat, wird unter dem Handler einer *anderen* Anfrage ausgeführt — mitsamt Rechte- und Methodenprüfungen, die nie für sie gedacht waren.

In den Logs sieht das nach fast nichts aus:

```text
POST //?_w2s=…  HTTP/2.0&quot;  207
```

Pfade mit doppeltem und dreifachem Schrägstrich, dazu `HTTP 207 Multi-Status`-Antworten. Dreißig Sekunden davon. Wer nicht gezielt nach dem Batch-Endpunkt sucht, liest das als Rauschen.

**Schritt 2 — SQL-Injection ([CVE-2026-60137](https://www.cve.org/CVERecord?id=CVE-2026-60137)).** Die desynchronisierte Anfrage landet auf dem Parameter `author__not_in` von `WP_Query`. Als skalarer String statt als Array übergeben, rutscht er an der Bereinigung vorbei und wird direkt in rohes SQL interpoliert. Ein `UNION`-basierter Lesezugriff holt dann beliebige Werte aus der Datenbank, zurückgegeben in der Form eines Blogbeitrags.

Dafür gibt es ein verlässliches Erkennungsmerkmal. Inmitten einer Serie kleiner Batch-Antworten kam eine einzelne mit **1.242.468 Byte** zurück — drei Größenordnungen über ihren Nachbarn. Das ist kein API-Aufruf. Das ist eine Datenbank, die ausgelesen wird.

**Schritt 3 — Rechteausweitung ohne Passwort.** Das ist der raffinierte Teil. Das gefälschte Abfrageergebnis vergiftet den In-Memory-Objektcache von WordPress. Anschließend missbraucht der Angreifer die oEmbed-Funktion, um dieses vergiftete Objekt als echte `oembed_cache`-Zeile in der Datenbank zu verankern — und formt es über ein Cache-Abgleich-Gadget und eine selbstreferenzierende Parent-Schleife in ein `customize_changeset` um.

Das Anwenden eines Changesets läuft mit der Autorität von **Benutzer-ID 1**. In diesem Fenster geliehener Autorität wird eine zuvor abgewiesene Anfrage zum Anlegen eines Administrators erneut gesendet — und diesmal geht sie durch.

**Schritt 4 — Codeausführung.** Mit einem echten Administrator-Konto in der Hand meldete sich der Angreifer an und lud ein Plugin hoch. Das ist kein Exploit mehr. Das ist Tagesgeschäft. Das Plugin registrierte eine unauthentifizierte REST-Route, die base64-kodierte Shell-Befehle entgegennahm und deren Ausgabe zurückgab.

Zeit vom Erstkontakt bis zur funktionierenden Kommando-Shell: **93 Sekunden.**

## Die zwei Zeilen, die der Angreifer nicht löschen konnte

Der Exploit räumt auf. Er entfernt sein eigenes Administrator-Konto und beseitigt seine Webshell. Die Log-Aufbewahrung auf dem Host lag bei rund zehn Tagen — kürzer als die Verweildauer —, sodass die Logs von Tag null bereits rotiert waren, als jemand nachsah. Auf dem Papier hätte das ein Vorfall werden müssen, bei dem man sagt: *„vermutlich WordPress, vermutlich ein Plugin“* — und weitergeht.

Nur hinterlässt der Trick zur Rechteausweitung zwei Zeilen in der Datenbank, und die sind bauartbedingt schwer zu entfernen:

- eine `oembed_cache`-Zeile — das vergiftete Cache-Objekt
- eine `customize_changeset`-Zeile, `post_author = 1`, geschrieben in derselben Sekunde, in der der Fremd-Administrator angelegt wurde, noch immer mit der Markierung des Proof-of-Concept: `&quot;title&quot;:&quot;proof&quot;`

Beide lagen elf Tage später unverändert in der produktiven Datenbank.

Das ist der Unterschied zwischen einer vermuteten und einer bewiesenen Ursache. Diese Zeilen sind der Grund, warum im Bericht steht: *„die Kette lief um 16:11:32 durch und war erfolgreich“* — statt *„die Website wurde vermutlich über eine Schwachstelle kompromittiert“*. Der Angreifer hat hinter sich aufgeräumt — nur nicht dort, wo sich nichts wegräumen lässt.

Die andere Hälfte der Rettung war organisatorisch: **die Beweissicherung erfolgte vor der Bereinigung**, einschließlich eines vollständigen Server-Backups aus der Zeit davor. Als sich herausstellte, dass die Live-Logs von Tag null bereits rotiert waren, lagen sie in diesem Backup noch vor. Hätten wir zuerst bereinigt und danach untersucht — der Reflex, wenn die Domain eines Kunden Casino-Spam an Google ausliefert —, wäre das Einfallstor nicht mehr feststellbar gewesen.

&gt; Bereinigen kommt an zweiter Stelle. Was Sie löschen, um die Website zu retten, ist zugleich der Beweis dafür, wie jemand hineinkam.

## Ein Benutzer, vier Datenbanken

Die Kompromittierung reichte genau so weit, wie das Dateisystem es zuließ — und das betraf deutlich mehr als eine Website.

Auf dem Server lagen fünf WordPress-Websites. **Vier davon liefen unter einem einzigen Systembenutzer.** Die allerersten Befehle des Angreifers nach Erlangen der Shell lasen sämtliche `wp-config.php`-Dateien auf der Maschine aus und durchsuchten das Dateisystem nach Passwörtern, API-Schlüsseln und Tokens — also Datenbank-Zugangsdaten und Authentifizierungs-Salts für alle vier, nicht nur für die verwundbare.

Die fünfte Website lief unter einem eigenen Systembenutzer. Sie wurde sondiert. Sie blieb unberührt. Derselbe Server, derselbe Angreifer, dieselben zehn Tage — der einzige Unterschied war eine Benutzergrenze.

Das ist das gesamte Argument für saubere Trennung, geliefert als kontrollierter Versuch, den niemand durchführen wollte. Aus einer Schwachstelle in einer Website wurde die Preisgabe der Zugangsdaten von vier Websites, allein wegen des Zuschnitts des Hostings.

Ebenso genau gehört die Grenze benannt: Der Angreifer agierte als Web-Benutzer, nie als root. Die SSH-Authentifizierungsprotokolle über den gesamten Zeitraum zeigen, dass sich der Exploit überhaupt nie per SSH authentifiziert hat, und es gab keine root-eigene Persistenz — kein Cron, keine SUID-Binaries, keine Schlüssel. Der Radius blieb auf die Web-Anwendungsebene beschränkt. Diese Ebene war nur *breiter*, als sie hätte sein müssen.

## Drei Zahlen, drei Versäumnisse

Lässt man die Technik beiseite, bleiben drei Zeiträume — und jeder davon ist ein eigenes Versäumnis:

**46 Tage** — so lange war der Patch öffentlich, bevor die Kompromittierung stattfand. Automatische Updates waren auf allen Websites deaktiviert. Das ist die Hauptursache und zugleich die billigste denkbare Abhilfe.

**10,3 Tage** — Verweildauer. Nichts schlug an, als ein unauthentifiziert erzeugtes Administrator-Konto auftauchte, ein Plugin-Verzeichnis mit zufälligem Hex-Namen erschien oder eine Backdoor **7.174-mal** aufgerufen wurde. Die Entdeckung kam von außen, über eine Suchmaschine, sechs Tage nachdem die Spam-Nutzlast überhaupt platziert worden war.

**~10 Tage** — Log-Aufbewahrung. Kürzer als die Verweildauer. Das ist die unauffällige Kennzahl — und die, die am häufigsten zu knapp bemessen wird: *Ihr Aufbewahrungsfenster muss länger sein als eine realistische Verweildauer, sonst laufen Ihre Logs ab, bevor Sie merken, dass Sie sie brauchen.* Zehn Tage Aufbewahrung bedeuten, dass jeder Vorfall, der länger als zehn Tage dauert, bauartbedingt allein aus Logs nicht mehr aufklärbar ist.

## Was ich jedem WordPress-Betreiber rate

Der konkrete Exploit ist in einem Jahr bedeutungslos. Sein Muster nicht.

- **Automatische Core-Updates aktivieren — und danach prüfen, ob sie gelaufen sind.** Nicht „wir haben einen Patch-Prozess“, sondern: nachsehen, ob eine Website tatsächlich die Version gewechselt hat. Der Fehlerfall hier waren deaktivierte Auto-Updates, die 46 Tage lang niemandem auffielen.
- **Ein Systembenutzer pro Website. Immer.** Das ist die eine Änderung, die aus der Preisgabe von vier Datenbanken einen Vorfall auf einer einzigen Website gemacht hätte. Beim Aufsetzen kostet sie nichts; nachträglich ist sie schmerzhaft.
- **Aufbewahrung länger ansetzen als die plausible Verweildauer.** Sechzig bis neunzig Tage. Wer leise hereinkommt, bleibt Wochen — und ein Zehn-Tage-Fenster garantiert, dass Sie einen bereits gewischten Tatort untersuchen.
- **Auf das Anlegen von Administrator-Konten alarmieren.** Ein seltenes, aussagekräftiges, trivial überwachbares Ereignis. Es hätte hier zehn Tage auf Minuten reduziert und kostet vielleicht eine halbe Stunde Einrichtung.
- **Entwicklungs-Websites aus dem öffentlichen Internet nehmen.** Die Staging-Kopie erhielt im selben Zeitraum rund 1.470 Exploit-Versuche. Dass sie nicht durchkamen, war Glück, keine Architektur.
- **Sichern, bevor Sie bereinigen.** Logs, Datenbank und Datei-Zeitstempel *zuerst* vom Host kopieren, hashen — und erst dann bereinigen.
- **Wo personenbezogene Daten betroffen sind, ist der technische Bericht nicht das letzte Wort.** Ein bestätigter unbefugter Zugriff auf Systeme mit personenbezogenen Daten kann Meldepflichten nach [DSGVO Art. 33 und 34](https://eur-lex.europa.eu/eli/reg/2016/679/oj) auslösen, und maßgeblich ist in der Regel die *hinreichende Wahrscheinlichkeit* eines Zugriffs, nicht der Nachweis eines Abflusses. Nachgewiesener Shell-Zugriff mit Lesezugriff auf Datenbanken ist für diese Prüfung erheblich. Die Entscheidung liegt bei der Rechtsberatung, nicht bei der Person, die die Forensik gemacht hat — deren Aufgabe ist es, die Fakten dafür zu liefern und ehrlich zu benennen, was die Logs nicht ausschließen können.

## Der unangenehme Teil

Ich kann exakt sagen, was der Angreifer getan hat, weil die Datenbank eine Quittung aufbewahrt hat. Ich kann sagen, was er zu lesen *imstande* war — vier Website-Datenbanken, einschließlich Kontaktformular-Einträgen —, weil die Shell zehn Tage lang genau diesen Zugriff hatte.

Was ich nicht sagen kann: was tatsächlich gelesen wurde. Die Zugriffsprotokolle erfassen keine Request-Bodies. Diese Information ist schlicht nicht mehr vorhanden, und keine noch so sorgfältige Forensik holt sie zurück.

Also steht genau das im Bericht, mit diesen Worten, und jede Feststellung ist als *Bestätigt* oder *Bewertet* gekennzeichnet. In der Incident Response gibt es eine echte Versuchung, eine Lücke in der Beweislage mit sicherem Ton zu überdecken, weil ein Kunde in einer schlechten Woche Gewissheit will. Sie ihm zu geben, obwohl man sie nicht hat, ist genau der Weg, auf dem ein technischer Bericht am Ende eine rechtliche Entscheidung in die Irre führt.

Die Eindämmung war in weniger als einem Tag erledigt, nachdem wir Bescheid wussten. Bis wir überhaupt Bescheid wussten, vergingen zehn Tage — und kam nur zustande, weil eine Suchmaschine uns darauf gestoßen hat.

Langweilige, überprüfbare Verteidigung schlägt geschickte Reaktion. Fast immer.</content:encoded><category>Security</category><category>Incident Response</category><category>WordPress</category><category>Forensik</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item><item><title>Cyber-Risiko in Euro beziffern, nicht in Ampelfarben</title><link>https://szelcsanyi.net/de/blog/pricing-cyber-risk-in-euros</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/pricing-cyber-risk-in-euros</guid><description>Dieses Jahr habe ich Nisura gebaut — eine europäische Engine zur Quantifizierung von Cyber-Risiken. Hier sind die Modellierungsentscheidungen, auf die es ankam, einschließlich jener, über die ich wochenlang mit mir selbst gestritten habe.</description><pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate><content:encoded>Jedem Vorstand, vor dem ich gesessen habe, wurde dasselbe Artefakt gezeigt: ein Raster aus roten, gelben und grünen Kästchen. Jemand geht es durch, alle nicken, die Sitzung geht weiter. Dann stellt die Finanzvorständin die einzige Frage, auf die es wirklich ankommt — *was kostet uns das gerade, und wie viel würde es sparen, das zu beheben?* — und im Raum wird es still.

Diese Stille ist der Grund, warum es [Nisura](https://nisura.eu) gibt. Es ist eine Engine zur Quantifizierung von Cyber-Risiken für den europäischen Markt: Sie erfasst die externe Angriffsfläche und löst sie in einen Eurobetrag auf, den ein Vorstand gegen die Kosten von Maßnahmen abwägen kann. In diesem Beitrag geht es um die Modellierung dahinter — einschließlich der Entscheidung, die ich zuerst falsch getroffen habe.

## Schweregrad ist nicht dasselbe wie Gefahr

Der erste Reflex ist, nach CVSS zu sortieren. Das ist der Wert, den alle kennen, er steht in jedem Advisory und liegt direkt im Feed bereit. Für die Entscheidung, was man am Montag behebt, ist er nahezu unbrauchbar.

CVSS beantwortet die Frage, *wie schlimm es wäre, wenn jemand das ausnutzt*. Es sagt nichts darüber, ob das gerade tatsächlich jemand tut. Die Folge ist eine Warteschlange, sortiert nach theoretischem Schadensmaximum, in der eine 9,8 steht, die nie jemand zu einer Waffe gemacht hat — über einer 7,5, die drei Ransomware-Gruppen heute aktiv einsetzen.

Deshalb sortiert die Engine stattdessen nach Ausnutzungsnachweis: nach dem Katalog bekannter ausgenutzter Schwachstellen der CISA, den EPSS-Wahrscheinlichkeiten von FIRST und den SSVC-Entscheidungspunkten der CISA-ADP (Act / Attend / Track). Kombiniert man das, kehrt sich die Reihenfolge auf eine Weise um, die zunächst falsch aussieht — bis man eine Weile damit gearbeitet hat.

## Das amerikanische Modell lässt sich nicht umrechnen

Das ist die Entscheidung, an der ich am längsten gesessen habe, und mein erster Instinkt war falsch.

Die ausgereiften Schadensmodelle in diesem Feld beruhen auf US-amerikanischen Versicherungsdaten. Das sind wirklich gute Daten — jahrzehntelang gesammelt, versicherungsmathematisch sauber, weit reichhaltiger als alles Vergleichbare in Europa. Mein ursprünglicher Plan war, sie zu verwenden und das Ergebnis in Euro umzurechnen.

Das funktioniert nicht, und der Grund ist nicht der Wechselkurs.

Ein Modell aus Schadensfällen erbt die Form der Policen, die dahinterstehen. Es ist durch das begrenzt, was Versicherer zu zahlen bereit waren — es deckelt Ihr Risiko also stillschweigend bei der Deckungssumme und blendet genau die Teile aus, die am meisten wehtun: Selbstbehalte, abwandernde Kunden, Reputationsschäden, verlorenes geistiges Eigentum, die achtzehn Monate verschobene Produktentwicklung nach einem Vorfall. Sie messen nicht Ihre Exponierung. Sie messen die Bereitschaft eines Dritten, einen Ausschnitt davon zu zeichnen.

Hinzu kommt, dass die Eingangsgrößen für europäische Verhältnisse falsch sind. Ausfallkosten sind eine Funktion der Arbeitskosten, und Arbeitskosten in Österreich sind nicht Arbeitskosten in Texas. Betriebsunterbrechung hängt von der operativen Marge je Branche ab, und europäische Margen unterscheiden sich strukturell. Regulatorische Exponierung ist hier kein Rundungsfehler — NIS2 und DSGVO haben gesetzliche Obergrenzen, die unterschiedlich berechnet werden, und DORA betrifft einen ganz anderen Kreis von Unternehmen.

Die Engine ist deshalb andersherum gebaut: auf Arbeitskostenindizes von Eurostat, Margendaten von Damodaran für Europa, der NACE-Branchensystematik, die NIS2 selbst verwendet, und der EUVD der ENISA als vollwertiger Schwachstellenquelle neben den amerikanischen Feeds. Die Bedrohungsdaten sind global, weil Angreifer es auch sind. Das Schadensmodell ist europäisch, weil die Schäden es sind.

Das ist auch der Teil, der sich am schwersten kopieren lässt. An dieselben öffentlichen Feeds kann sich jeder anschließen — die sind Grundausstattung. Das europäische Kostenmodell zu bauen ist die langsame, unspektakuläre Arbeit, die das Ergebnis tatsächlich unterscheidbar macht.

## Eintrittswahrscheinlichkeit und Schadenshöhe sind zwei Hebel

Die zweite Modellierungsentscheidung war, zwei Dinge zu trennen, die die meisten Werkzeuge vermischen.

Mehrfaktor-Authentifizierung für externe Zugänge verändert die *Wahrscheinlichkeit* eines Einbruchs. Geprüfte Offline-Backups tun das nicht — sie verändern, was er Sie *kostet*, wenn er trotzdem passiert. Das sind mathematisch verschiedene Operationen, und sie in einen einzigen „Sicherheitsscore“ zusammenzufalten zerstört genau die Information, die eine Entscheiderin braucht.

Maßnahmen und Resilienz werden deshalb als getrennte Ebenen modelliert:

- **Maßnahmen** — MFA, EDR, schnelles Patchen, Segmentierung, kein offenes RDP — senken die modellierte Eintrittswahrscheinlichkeit.
- **Resilienz** — unveränderliche Backups, ein geprüfter Incident-Response-Retainer, ein geübter Notfallplan — senken den verbleibenden Schaden.

Der praktische Gewinn: Eine CISO sieht, bevor sie einen Euro ausgibt, welcher Hebel ihre Zahl weiter bewegt. Manchmal ist die Antwort eine Maßnahme für 40.000 Euro. Oft ist es eine geübte Rücksicherung, die ein Wochenende kostet. Ein einziger gemischter Score kann Ihnen das nie sagen.

Der erwartete Jahresschaden ergibt sich dann auf die naheliegende Weise — Eintrittswahrscheinlichkeit mal Schadenshöhe, summiert über die modellierten Szenarien — und wird als Verlustüberschreitungskurve dargestellt statt als eine Zahl, weil die interessanten Entscheidungen im Randbereich liegen. Der erwartete Jahresschaden sagt Ihnen, was Sie budgetieren sollten. Der wahrscheinliche Maximalschaden sagt Ihnen, was das Unternehmen beenden könnte. Das gehören zwei getrennte Zeilen.

## Was NIS2 Artikel 21 tatsächlich verlangt

Um NIS2 gibt es viel Lärm, das meiste davon verkauft Angst vor der 10-Millionen-Obergrenze und der persönlichen Haftung von Führungskräften. Beides ist real. Keines davon ist der nützliche Teil.

Der nützliche Teil ist, dass Artikel 21 *fortlaufendes* Risikomanagement und die Bewertung der Lieferkette verlangt und Absatz 2 Buchstabe g Cyberhygiene und Schulungen abdeckt. „Fortlaufend“ ist das tragende Wort. Ein jährlicher Penetrationstest ist in dem Moment veraltet, in dem er gedruckt wird, und ein Lieferantenfragebogen ist eine Selbstauskunft, die niemand prüft. Beides übersteht keine Prüferin, die wissen will, wie Ihre Lage im März aussah.

Fortlaufende externe Überwachung erzeugt etwas, das eine Momentaufnahme strukturell nicht liefern kann: einen mit Zeitstempeln versehenen, nachprüfbaren Verlauf Ihrer Exponierung — wann sie bestand und was Sie dagegen unternommen haben. Das ist das Compliance-Artefakt: kein Zertifikat, sondern eine Historie.

Dieselbe Logik hat auch das menschliche Risiko ins Modell gebracht. Autorisierte Phishing-Simulationen gegen die eigene Belegschaft liefern eine echte Klickrate, die als Faktor auf den Angriffsweg über Zugangsdaten einfließt, statt als separater Bericht zu enden, den niemand liest. Nur aggregiert, nie werden Zugangsdaten erfasst, abgesichert durch eine unterschriebene Beauftragung und in Österreich und Deutschland durch eine Betriebsratszustimmung. Diese letzte Auflage ist hier kein Nice-to-have — macht man das falsch, hat man ein arbeitsrechtliches Problem geschaffen, während man ein Sicherheitsproblem lösen wollte.

## Der Teil, der mich ehrlich hält

Der naheliegende Fehlermodus dieser ganzen Kategorie ist Scheingenauigkeit. Eine Zahl wie *1.250.400 €* liest sich, als hätte sie jemand nachgezählt. Niemand hat sie nachgezählt. Sie ist das Ergebnis eines probabilistischen Modells mit echter Unsicherheit in jeder Eingangsgröße, und sie als harte Zahl zu präsentieren lädt genau zu der Selbstsicherheit ein, die man Heatmaps vorwirft.

Diese Spannung habe ich nicht vollständig gelöst. Was bisher hilft: die Kurve zeigen statt der Punktschätzung, die Annahmen einsehbar halten, für jede Kostenbasis die Quelle angeben und deutlich sagen, dass dies eine Entscheidungshilfe ist und weder eine Prüfung noch ein versicherungstechnisches Gutachten. Die ehrliche Einordnung lautet: Die Zahl ist nicht *richtig* — sie ist *begründbar*, und sie ist in einer Richtung falsch, die man einsehen und bestreiten kann. Das ist eine echte Verbesserung gegenüber Gelb, aber es ist nicht dasselbe wie Wahrheit.

Die andere Auflage, die ich mir früh gesetzt habe: Die kostenlose Erstanalyse schickt kein einziges Paket an Ihre Infrastruktur. Nur passives OSINT, keine Anmeldung, keine Agenten. Zum Teil ist das eine Vertrauensfrage — um Erlaubnis für einen Scan zu bitten, bevor man sich irgendetwas verdient hat, ist ein schlechter erster Kontakt. Vor allem aber erfordert die Alternative ein Gespräch über Beauftragung, für das das Werkzeug noch niemandem einen Anlass gegeben hat.

## Was ich jemandem raten würde, der damit anfängt

Bauen Sie das Kostenmodell vor dem Scanner. Die Feeds sind Massenware und das Scannen ist gelöst; was das Ergebnis aussagekräftig macht, ist die unspektakuläre wirtschaftliche Modellierung darunter — und die dauert weit länger, als Sie einplanen.

Und widerstehen Sie dem Score. Jedes Gespräch wird Sie in Richtung einer Zahl drängen, einer Note, einer Farbe, weil man es gewohnt ist, so etwas in die Hand zu bekommen. Der gesamte Wert liegt in der Weigerung — Wahrscheinlichkeit und Schadenshöhe getrennt halten, Unsicherheit sichtbar lassen, Annahmen einsehbar machen —, damit jemand, der dem Ergebnis widerspricht, auf *die* Eingangsgröße zeigen kann, die er für falsch hält.

Dieser Streit ist das Produkt bei der Arbeit. Eine Heatmap hat noch nie jemandem etwas gegeben, das konkret genug war, um darüber zu streiten.</content:encoded><category>Security</category><category>Cyber Risk</category><category>NIS2</category><category>Product</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item><item><title>Sechs Stunden bis zur Abwehr — Anatomie einer Verbindungsflut</title><link>https://szelcsanyi.net/de/blog/anatomy-of-a-dos-attack</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/anatomy-of-a-dos-attack</guid><description>Ein ungeschützter Origin-Server bekam eine anhaltende Flut auf Port 443, bis ihm die Dateideskriptoren ausgingen. Wie wir das diagnostiziert, an einem Nachmittag gestoppt haben — und warum es vermeidbar war.</description><pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate><content:encoded>*Dies ist eine anonymisierte Aufarbeitung eines realen Vorfalls, den ich bearbeitet habe. Kunde, IP-Adressen, Hostnamen und weitere identifizierende Angaben wurden entfernt oder verallgemeinert — es geht ausschließlich um die technischen Lehren.*

An einem Morgen war die Website eines Kunden — eine Marketing-Seite auf einem einzelnen Cloud-VPS — über HTTPS nicht mehr erreichbar. Nicht langsam. Weg. `curl` lieferte `HTTP 000`, der Browser drehte sich nur noch. Einfaches HTTP auf Port 80 antwortete weiterhin sofort, SSH lief, und die Maschine reagierte die ganze Zeit auf Ping. Genau diese Aufteilung — alles gesund außer 443 — ist die Signatur eines bestimmten Angriffstyps, und sie sagt einem, wo man suchen muss.

Am Ende des Nachmittags war der Angriff vollständig abgewehrt. Hier ist die Anatomie.

## Was tatsächlich passiert ist

Der Server bekam eine **Verbindungsflut auf Port 443**. Der Angreifer öffnete TLS-Verbindungen, so schnell er konnte, und schickte nie eine vollständige HTTP-Anfrage hinterher — gerade genug, damit nginx den Socket annahm und einen Dateideskriptor dafür reservierte. Multipliziert mit tausenden Verbindungen pro Sekunde erschöpft das die eine Ressource, an die niemand denkt, solange sie da ist: **offene Dateideskriptoren**.

War das Limit des Betriebssystems erreicht, konnte nginx *überhaupt keine* Verbindung mehr annehmen — weder vom Angreifer noch von echten Besuchern. Die Website war über HTTPS praktisch offline, während der Prozess festhing und nicht mehr vorankam.

## Die Spuren

Die Geschichte stand deutlich in den Logs — wenn man wusste, welches Log man lesen muss.

Das **Error-Log** von nginx war voll mit denselben zwei Zeilen, zehntausendfach:

```text
accept4() failed (24: Too many open files)
1024 worker_connections are not enough, reusing connections
```

Das **Access-Log** von nginx war im selben Zeitraum dagegen **vollständig leer**. Das ist der entscheidende Hinweis: Die Verbindungen erreichten nie die HTTP-Ebene. Sie starben bei Accept und TLS, es gab also nichts, was sich als Anfrage hätte protokollieren lassen. Wer nur Access-Logs und Dashboards zur Anfragerate beobachtet, für den ist ein solcher Angriff unsichtbar — die Anfragen, die man zählen würde, kommen nie zustande.

Die Messwerte des Hosts bestätigten das Bild:

- **CPU:** stundenlang bei 100 bis 150 Prozent festgenagelt, mit Spitzen über 200, gegenüber rund 5 Prozent im Ruhezustand. TLS-Handshakes sind das Teuerste, was ein HTTPS-Server tut, und der Angreifer erzwang eine Flut davon.
- **Eingehende Pakete:** rund 8.000 bis 10.000 pro Sekunde, bei nur etwa 1 MB/s. Dieses Verhältnis — viele Pakete, wenig Bandbreite, im Schnitt etwa 100 Byte pro Paket — spricht eindeutig für Verbindungsaufbau und nicht für echte Nutzlast.
- **Port 80:** antwortete durchgehend in 0,08 Sekunden mit einer 301-Weiterleitung. **Port 443:** tot.

Keine Anzeichen für Datenabfluss oder Kompromittierung. Das war reine Ressourcenerschöpfung — ein Denial of Service, nicht mehr, aber auch nicht weniger.

## Die Ursache (es war nicht der Angreifer)

Ob solche Angriffe gelingen, entscheidet sich an dem, was man *vorher* getan hat. Hier war der Origin-Server von einem Dritten aufgesetzt worden, ohne jede Grundabsicherung:

- **Die Origin-IP war direkt erreichbar.** Kein Reverse Proxy, kein CDN davor. Die öffentliche IP war über DNS auflösbar und wurde sogar im Reverse-DNS-Eintrag des Hosters preisgegeben — der Angreifer konnte also direkt auf die Maschine zielen und jede namensbasierte Abwehr umgehen.
- **Praktisch keine Firewall.** Port 443 nahm Verbindungen aus dem gesamten Internet an.
- **nginx war für eine Spielzeuglast konfiguriert.** `worker_connections` stand auf dem Standardwert `1024`, `worker_rlimit_nofile` war nie angehoben worden — die Obergrenze für Dateideskriptoren lag entsprechend niedrig und war leicht zu erreichen.
- **Keine Protokollierung auf Paketebene.** Das machte die nachträgliche Analyse deutlich mühsamer als nötig.

Die unbequeme Erkenntnis: Die Website war von einer Marketingagentur aufgesetzt worden, ohne jede sicherheitstechnische Prüfung. Das ist die eigentliche Schwachstelle — **wer die Website baut, sollte nicht zugleich das letzte Wort über ihre Absicherung haben, ohne dass jemand für die Härtung verantwortlich ist.**

## Die Abwehr

Die Maßnahme war gestaffelt: erst den Origin verbergen, dann nur noch den Schutzschild mit ihm sprechen lassen.

1. **Einen Reverse Proxy davorsetzen (Cloudflare).** Sämtliche DNS-Einträge der Domain wurden auf *proxied* umgestellt, sodass der gesamte Verkehr nun über das globale Netz des CDN läuft. TLS terminiert an dessen Edge, nicht mehr am Origin — und entscheidend: **die echte Server-IP ist von außen nicht mehr sichtbar**.
2. **Die Origin-Firewall auf die IP-Bereiche des CDN einschränken.** In der Host-Firewall wurden die Ports 443 und 80 auf *ausschließlich* die veröffentlichten IP-Bereiche des Proxys begrenzt (eine Handvoll IPv4- und IPv6-CIDR-Blöcke). Jeder andere Verbindungsversuch auf 443 wird nun am Netzrand verworfen, bevor er nginx überhaupt erreicht. Diese eine Regel senkte den Angriffsverkehr auf nahezu null.
3. **Die DNS-Hoheit zum Proxy verlagern**, um dessen Caching, WAF und DDoS-Funktionen durchgängig nutzen zu können.
4. **Den „Under Attack“-Modus aktivieren** — eine JavaScript-Challenge, die automatisierte Skripte und Bots herausfiltert und echte Besucher nach einer kurzen Zwischenseite durchlässt.
5. **Protokollierung auf Paketebene einrichten** (eine `iptables`-Log-Regel auf 443, über Neustarts hinweg persistent), damit der nächste Vorfall mit Beweisen beginnt und nicht mit Vermutungen.
6. **Missbrauchsmeldungen** bei den Hostern einreichen, aus deren Netzen der Verkehr stammte.

Innerhalb weniger Minuten nach dem Greifen der Firewall-Regel fiel die CPU auf Leerlauf zurück, die eingehenden Pakete gingen gegen null. Die Graphen des Hosts zeigten die Wirkung unmissverständlich: erst eine Wand aus Last, dann eine flache Linie.

## Was ich jedem mit einem eigenen Origin-Server rate

Der konkrete Angriff ist weniger wichtig als das Muster. Fünf Dinge hätten diesen Vorfall verhindert oder zumindest deutlich abgeschwächt:

- **Die Origin-IP niemals preisgeben.** Von Anfang an ein CDN oder einen Proxy davorsetzen und die Origin-IP wie ein Geheimnis behandeln. Die meisten dieser Angriffe verpuffen in dem Moment, in dem der Angreifer die Maschine nicht mehr findet.
- **Die Origin-Firewall auf die Bereiche des Proxys beschränken.** Ein CDN davor nützt nichts, solange der Angreifer die nackte IP weiterhin direkt erreicht. 80 und 443 auf die CIDR-Blöcke des Proxys begrenzen — das war hier die Regel, die die eigentliche Arbeit geleistet hat.
- **Für den Fehlerfall konfigurieren, nicht für die Demo.** `worker_connections` (4096 oder mehr) und `worker_rlimit_nofile` anheben, damit erschöpfte Dateideskriptoren nicht sofort alles lahmlegen.
- **Auf das richtige Signal schauen.** Dashboards zur Anfragerate zeigen eine Verbindungsflut nicht — die Anfragen kommen ja nie zustande. Beobachten Sie Verbindungszahlen, Pakete pro Sekunde, CPU und die *Error*-Logs.
- **Die Verantwortung für Sicherheit klar zuordnen.** Wer die Website baut, sollte nicht das letzte Wort darüber haben, wie sie verteidigt wird. Eine kurze Härtungsprüfung vor dem Livegang hätte jede der oben genannten Lücken geschlossen.

Der Angriff war in etwa sechs Stunden erledigt, wovon der größte Teil auf Diagnose und das korrekte Aufsetzen des Schutzschilds entfiel. Die Abwehr selbst — Origin verborgen, Firewall auf den Proxy beschränkt — ist nun dauerhaft. Solange die Origin-IP nicht öffentlich wird und der Proxy davor bleibt, hat derselbe Angriff schlicht kein Ziel mehr.

Langweilige, gestaffelte Verteidigung schlägt geschickte Reaktion. Fast immer.</content:encoded><category>Security</category><category>Incident Response</category><category>DevOps</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item><item><title>Bewusst langweilige Software ausliefern</title><link>https://szelcsanyi.net/de/blog/ship-boring-software</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/ship-boring-software</guid><description>Die verlässlichsten Systeme, die ich gebaut habe, waren auch die am wenigsten cleveren. Warum ich zuerst zum Langweiligen greife — und wann ich meine eigene Regel breche.</description><pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate><content:encoded>Früh in meiner Laufbahn habe ich Neuheit mit Qualität verwechselt. Ein neuer Datenspeicher, eine exotische Queue, ein Framework, das seit drei Wochen auf Hacker News steht — jedes davon fühlte sich nach Fortschritt an. Die meisten wurden zu dem, weswegen ich um drei Uhr nachts angerufen wurde.

Heute optimiere ich auf eine andere Eigenschaft: **Wie schnell versteht die nächste Entwicklerin das hier und kann es gefahrlos ändern?** Diese Frage führt in aller Regel zu langweiliger, gut verstandener Technik — und sie war bisher der beste Vorhersagewert dafür, ob ein System gut altert.

## Langweilig ist ein Vorzug, kein Kompromiss

Langweilige Technik hat eine große Fläche an Vorerfahrung. Wenn Postgres etwas Überraschendes tut, sind zehntausend Leute vorher darüber gestolpert und haben es aufgeschrieben. Wenn Ihre selbstgebaute Konsensschicht etwas Überraschendes tut, sind Sie die Dokumentation.

Das zählt genau dann am meisten, wenn es brennt:

- Die Fehlerbilder sind bekannt und auffindbar.
- Einstellungen sind leichter, weil die Fähigkeiten bereits existieren.
- Das Umfeld — Backups, Migrationen, Observability — ist ausgereift.

Nichts davon ist glamourös. Alles davon summiert sich.

## Wann ich meine Regel breche

Standardmäßig langweilig heißt nicht immer langweilig. Ich gebe Neuheitsbudget aus — was Dan McKinley [Innovation Tokens](https://mcfunley.com/choose-boring-technology) nannte, von denen man etwa drei hat —, wenn drei Dinge zusammenkommen:

1. Das Problem gehört wirklich zum Kern des Geschäfts und ist nicht nebensächlich.
2. Die langweilige Option hat eine konkrete, gemessene Obergrenze, an die ich bereits gestoßen bin.
3. Ich kann das Neue hinter einer Schnittstelle einkapseln, die ich in einer Woche wieder herausreißen könnte.

&gt; Eine gute Architektur macht die umkehrbaren Entscheidungen billig und die unumkehrbaren selten.

Wenn ich die Linie nicht ziehen kann, an der das riskante Bauteil endet, bin ich noch nicht so weit, es hinzuzufügen.

## Der Test, den ich tatsächlich anwende

Bevor ich etwas in einen Stack aufnehme, frage ich: *Wenn das zum denkbar schlechtesten Zeitpunkt kaputtgeht — weiß ich, wie ich es repariere, und kann ich diese Reparatur jemandem erklären, der es noch nie gesehen hat?*

Lautet die Antwort nein, ist die interessante Wahl meistens die falsche. Liefern Sie die langweilige Variante aus, messen Sie sie — und lassen Sie die Daten entscheiden, wann es Zeit für etwas Schärferes ist, nicht das Changelog.</content:encoded><category>Architecture</category><category>Reliability</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item><item><title>Wie ich eine langsame Query lese, bevor ich sie anfasse</title><link>https://szelcsanyi.net/de/blog/reading-a-slow-query</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/reading-a-slow-query</guid><description>Ein wiederholbarer Weg von „die Datenbank ist langsam“ zu einer konkreten, gemessenen Lösung — ohne Raten und ohne wahllos Indizes anzulegen.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>„Die Datenbank ist langsam“ ist kein Fehlerbericht — das ist ein Gefühl. Meine Aufgabe ist es, daraus eine Zahl zu machen und aus dieser Zahl einen Plan. Hier ist die Schleife, die ich in jedem Performance-Mandat durchlaufe, mehr oder weniger unverändert.

## 1. Mit einer echten Query nachstellen

Vom Dashboard aus zu raten ist der sichere Weg, das Falsche zu optimieren. Ich fange damit an, genau das Statement und die Parameter zu erfassen, die wehtun — am besten aus `pg_stat_statements`, sortiert nach Gesamtzeit statt nach Mittelwert. Die Abfrage, die 8 Millisekunden braucht und eine Million Mal am Tag läuft, ist wichtiger als der Zwei-Sekunden-Bericht, den niemand ansieht.

## 2. Den Planer fragen, nicht vermuten

`EXPLAIN (ANALYZE, BUFFERS)` ist das ganze Spiel. Es zeigt, was der Planer *tatsächlich getan hat*, nicht, was man sich erhofft hatte:

```sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE tenant_id = $1 AND status = &apos;open&apos;
ORDER BY created_at DESC
LIMIT 50;
```

Zwei Dinge lese ich zuerst:

- **Geschätzte gegenüber tatsächlichen Zeilen.** Eine große Abweichung bedeutet, dass der Planer mit schlechten Statistiken arbeitet und in der Folge schlechte Entscheidungen trifft.
- **Buffers.** Wenn die gelesenen Heap-Blöcke die zurückgegebenen Zeilen um ein Vielfaches übersteigen, hole ich weit mehr Daten, als die Antwort braucht.

## 3. Das eigentliche Problem benennen

Die meisten langsamen Abfragen, die mir begegnen, gehören zu einer kurzen Liste:

1. Ein fehlender oder falscher Index — ein sequenzieller Scan dort, wo ein Filter hätte eingrenzen sollen.
2. Eine schlechte Join-Reihenfolge aufgrund veralteter Statistiken.
3. Breite Zeilen holen, obwohl nur wenige Spalten gebraucht werden.
4. In der Datenbank tun, was ein Cache erledigen sollte.

&gt; Die Kategorie zu benennen ist 80 Prozent der Lösung. Erst wenn ich sagen kann, *welche* davon es ist, erlaube ich mir, SQL zu schreiben.

## 4. Eine Sache ändern, erneut messen

Dann — und erst dann — nehme ich genau eine Änderung vor: den zusammengesetzten Index anlegen, den Join umschreiben, eine Spalte in den Index aufnehmen. Ich führe dasselbe `EXPLAIN ANALYZE` erneut aus und vergleiche. Eine Variable nach der anderen, jedes Mal, damit ich die Verbesserung ehrlich zuordnen kann.

Die Disziplin liegt nicht darin, clevere Kniffe zu kennen. Sie liegt darin, sich Schritt vier zu verweigern, bis die Schritte eins bis drei langweilig und nachweisbar erledigt sind.</content:encoded><category>Postgres</category><category>Performance</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item><item><title>Was ich in den ersten 90 Tagen eines Mandats mache</title><link>https://szelcsanyi.net/de/blog/freelance-first-90-days</link><guid isPermaLink="true">https://szelcsanyi.net/de/blog/freelance-first-90-days</guid><description>Wie ich mir als externer Entwickler Vertrauen verdiene — indem ich etwas Kleines, Echtes ausliefere, bevor ich irgendetwas Großes vorschlage.</description><pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate><content:encoded>Der schnellste Weg, einen Kunden zu verlieren, ist, drei Wochen an einer Strategiepräsentation zu sitzen. Der schnellste Weg, ihn zu behalten, ist, bis Freitag etwas auszuliefern, das er spüren kann. So strukturiere ich den Beginn eines neuen Mandats, damit früh Fahrt aufkommt — und sie dann auch bleibt.

## Woche 1–2: Kontext aufbauen, keine Meinungen

Ich lese den Code, die Störungen und den Kalender — in dieser Reihenfolge. Die Störungen zeigen mir, wo es tatsächlich wehtut; der Kalender zeigt mir, wie das Team wirklich arbeitet. Meine architektonischen Meinungen halte ich bewusst locker. Die meisten Systeme, die von außen falsch aussehen, sind eine vernünftige Antwort auf eine Rahmenbedingung, die ich noch nicht sehe.

Worauf ich früh bestehe, ist Zugang: Repository, Staging, Logs und ein Weg, zu deployen. Wenn ich nicht gefahrlos in die Produktion komme, kann ich Ihnen auch nicht gefahrlos helfen.

## Woche 3–6: eine echte Sache ausliefern

Bevor ich irgendetwas Großes vorschlage, bringe ich eine sinnvolle, sichtbare Arbeit vollständig zu Ende. Eine Latenzverbesserung mit einem Graphen, der sich bewegt hat. Ein wackliges Deployment, das langweilig geworden ist. Etwas, das sich das Team gewünscht hat und wofür nie Zeit war.

Das bewirkt zweierlei:

- Es zeigt, dass ich in *Ihrem* System arbeiten kann und nicht nur allgemein über Systeme rede.
- Es gibt uns einen gemeinsamen, konkreten Bezugspunkt für jedes weitere Gespräch.

&gt; Vertrauen entsteht nicht durch einen Titel. Es wird in kleinen Raten zurückgezahlt — durch Arbeit, die ankommt.

## Woche 7–12: mich selbst ersetzbar machen

Das Ziel guter Beratung ist, das Team stärker zurückzulassen, als eine Festanstellung es gekonnt hätte. In der zweiten Hälfte geht es deshalb um Übergabe, nicht um Heldentaten:

1. Das Runbook für das schreiben, was ich gerade repariert habe.
2. Gemeinsam programmieren, damit mindestens zwei Personen es ohne mich betreuen können.
3. Eine kurze, ehrliche Einschätzung hinterlassen, was ich als Nächstes tun würde — und was nicht.

Wenn ich meine Arbeit gut gemacht habe, braucht mich das Team am Ende nicht mehr. Das ist der Zweck der Sache — und zugleich der Grund, warum man mich für das nächste Mandat wieder anruft.</content:encoded><category>Consulting</category><category>Process</category><author>boris@szelcsanyi.net (Boris Szelcsányi)</author></item></channel></rss>