
Lab zur Erkennung persistenter Revocation: wenn das Zurücksetzen des Passworts erfolgreich ist, der Angreifer aber nie geht. Reproduziert den bedingten Revocation-Bug Strapi CVE-2026-22706, seinen Fix, ein Erkennungspaket mit drei Regeln und die naive Regel, die ihn übersieht.
Wenn das Zurücksetzen des Passworts gelingt, aber der Angreifer nie verschwindet.
Ein lokales Rot/Blau-Labor für eine einzige Schwachstellenklasse: die Anmeldedaten, die das Ereignis überleben, das sie eigentlich hätte vernichten sollen. Es liefert den Exploit, die Grundursache, den Fix, ein Erkennungspaket mit drei Regeln, ein Zustands-Audit für das, was die Regeln strukturell nicht sehen können, eine forensische Konsole — und die Erkennungsregel, die nicht funktioniert, im Repository belassen, um ihr Scheitern vorzuführen.
Schnellstart · Der Befund · Warum naive Erkennung scheitert · Das Regelpaket · Konsole · Der Fix · Matrix · Tests · Dokumentation
Derselbe Angriff. Dieselben Anfragen. Ein Unterschied.
Generiert von scripts/figures.py aus derselben Payload, die die Konsole zeichnet — in CI neu generiert und diffed, damit eine Abbildung nicht vom Code abweichen kann.
09:00 alice logs in ┐ refresh credential rt-001 │ the attacker steals rt-001 ┘
10:00 alice changes her password ← the one thing a victim can do alone HTTP 200 · password changed · fresh session issued
10:00:03 attacker: POST /refresh rt-002 → HTTP 200 new access credential at-004, issued 10:00:03
10:01:00 attacker: GET /me at-004 → HTTP 200 {"username": "alice", "authenticated": true}
Kein Fehler. Keine Anomalie. Keine fehlgeschlagene Authentifizierung zum Zählen. Die Zugangsdaten des Angreifers sind *drei Sekunden alt* und wurden vom Server auf Anfrage nach dem Reset erzeugt.
Hier ist die gesamte Schwachstelle:```python
def _revoke_for_security_change(self, user, kind, device_id):
if device_id:
revoke_credentials(user, device_id=device_id) # ← the finding
Lesen Sie das so, wie es ein Reviewer tun würde. Die Revocation-Logik ist direkt dort. Sie ruft die richtige Funktion mit dem richtigen Scope auf. Der Endpunkt drumherum aktualisiert den Passwort-Hash, gibt 200 zurück und stellt eine frische Session aus — jedes beobachtbare Verhalten einer korrekten Passwortänderung ist vorhanden.
Und wenn der Aufrufer device_id weglässt, wird nichts widerrufen, und der Endpunkt meldet trotzdem Erfolg.
Das ist nicht hypothetisch. Es ist
CVE-2026-22706
in Strapi ≤ 5.33.2, wo der Refresh-Token-Invalidierungsschritt von einem vom Aufrufer gelieferten deviceId abhängig war. Bewertet mit 2.1, Low. Diskutiert
unten.
Die Bewertung ist niedrig, weil der Angreifer bereits Zugriff hatte — das ist die Eintrittsbedingung, und dieser Bug gewährt nichts Neues. Was er gewährt, ist Dauer, und er tut dies, indem er die eine Kontrolle bricht, die das Opfer selbst bedienen kann.
test_persistence_lasts_as_long_as_the_refresh_credential durchläuft sieben Tage simulierte Zeit, um das zu zeigen.Ein Low-bewerteter Bug in einem Containment-Pfad kostet mehr als ein Low-bewerteter Bug in einem Feature-Pfad, weil die Kosten während eines Vorfalls anfallen, wenn niemand Advisories liest.
Die Regel, die man zuerst schreibt:```text IF credential.issued_at < credential_change.timestamp: ALERT
Es ist keine dumme Regel. Sie ist billig, braucht ein Feld, liest sich wie die Definition
des Problems, und sie hat **recht im einfachen Fall** — ein Angreifer, der ein
gestohlenes *Access*-Token nach dem Reset verwendet, wird erwischt.
`test_the_naive_rule_catches_the_simple_case` bestätigt, dass sie funktioniert.
Beide Regeln stellen dieselbe Frage über dieselbe Credential. Sie lesen unterschiedliche
Felder, um sie zu beantworten, und die beiden Felder widersprechen sich:

**Drei Sekunden danach, oder eine Stunde davor — dieselbe Credential, zum selben
Zeitpunkt.** Die naive Regel fragt ein Feld ab, das der Angreifer kontrolliert, und ein
`POST /refresh` setzt es zurück.
Führe sie über das Request-Log aus, wo diese Abfrage tatsächlich geschrieben wird,
denn Request-Logs sind das, was man hat:```text
NAIVE DETECTOR (NAIVE-001), over the request log
Result: NO ALERT
Six requests were served to the attacker after the reset. Every one of them
carried at-004, minted at 10:00:03 -- three seconds *after* the password
change. By its own timestamp it is the newest credential on the account.
Es wäre einfach, dort aufzuhören, und unehrlich, also führt das Labor auch die fairste Version der naiven Regel aus — erweitert, um auch den Refresh-Endpunkt zu überwachen:```text NAIVE DETECTOR, widened to include POST /refresh
1 alert at 2026-09-11T10:00:03.000Z: the credential presented to /refresh was rt-002, issued 2026-09-11T09:30:00.000Z. Then it goes blind. 6 events follow that hop and it flags none of them, because every credential from there on carries a post-reset timestamp. Its incident covers 1 accepted request; the lineage rule's covers all of them.