Manche Sicherheits-Lücken sind in keiner OWASP-Cheat-Sheet-Liste und in keiner Vulnerability-Scanner-Datenbank — weil sie logische Fehler in der Anwendung selbst sind. Workflow lässt sich überspringen, Coupon-Codes lassen sich doppelt einlösen, parallele Anfragen produzieren widersprüchliche Zustände, Validierung passiert zur falschen Zeit. Dieser Artikel behandelt Business-Logic-Flaws und Race Conditions — zwei eng verwandte Klassen, die in Pentests oft die wertvollsten Funde liefern und in produktiven Systemen reale Schäden anrichten.

Was diese Klasse so schwer macht

Klassische Web-Schwachstellen — XSS, SQLi, CSRF — haben Signatur-Muster: bestimmte Eingabe-Strings, bestimmte API-Aufrufe, bestimmte Token-Strukturen. Scanner finden sie, weil das Muster eindeutig ist.

Business-Logic-Flaws dagegen entstehen aus der Kombination legitimer Operationen, die in einer Reihenfolge oder Anzahl ausgeführt werden, die der Entwickler nicht erwartet hat:

  • Bezahl-Workflow lässt sich überspringen, weil der Bezahl-Schritt nur durch Frontend-State enforced wird.
  • Coupon-Code wird im selben Sekundenbruchteil 100-mal eingelöst, weil keine Locking-Logik existiert.
  • Konto-Saldo wird parallel mit zwei Requests abgehoben — beide Requests sehen den ursprünglichen Saldo, beide ziehen ab, Konto wird negativ.
  • Passwort-Reset-Token bleibt nach Nutzung gültig, weil das Invalidieren erst nach der erfolgreichen Reset-Aktion passiert.

Diese Klasse verlangt Verständnis der Anwendung — nicht Suche nach Mustern. Genau deshalb sind Bug-Bounty-Reports in dieser Klasse meist die teuersten: sie werden manuell von Forschenden gefunden, die die Anwendung gründlich studiert haben.

Race Conditions: das Concurrency-Problem

Race Conditions sind eine spezifische Unterklasse: zwei oder mehr Operationen laufen parallel, und das Endergebnis hängt von der Reihenfolge ab, in der sie tatsächlich abgearbeitet werden.

Klassisches Beispiel: doppelte Coupon-Einlösung.

JavaScript coupon-race-condition.js
// Schadhaft: Race Condition möglich
async function redeemCoupon(userId, code) {
  const coupon = await db.coupons.findOne({ code, used: false });
  if (!coupon) throw new Error('Coupon ungültig oder bereits eingelöst');

  // Zwischen findOne und update liegt ein Zeitfenster.
  // Parallele Requests sehen alle: coupon.used === false.

  await db.coupons.update({ code }, { used: true, redeemedBy: userId });
  await db.users.update({ id: userId }, { $inc: { balance: coupon.amount } });
}

Was geht schief:

  1. User schickt 50 parallele Requests mit demselben Coupon-Code.
  2. Alle 50 lesen die DB: coupon.used === false.
  3. Alle 50 prüfen den Check — alle bestehen.
  4. Alle 50 führen den Update aus.
  5. Alle 50 erhöhen das User-Balance.

Statt einmal 10 EUR Gutschrift hat User jetzt 500 EUR.

Schutz-Patterns:

JavaScript coupon-atomar.js
// Sicher: atomare Operation auf DB-Ebene
async function redeemCoupon(userId, code) {
  // findAndModify liefert null, wenn Coupon schon eingelöst
  const coupon = await db.coupons.findOneAndUpdate(
    { code, used: false },
    { $set: { used: true, redeemedBy: userId } },
    { returnDocument: 'after' }
  );

  if (!coupon) throw new Error('Coupon ungültig oder bereits eingelöst');

  // Erst nach erfolgreicher atomarer Markierung den Balance erhöhen
  await db.users.update({ id: userId }, { $inc: { balance: coupon.amount } });
}

Die atomare DB-Operation findOneAndUpdate mit Filter used: false lässt nur einen parallelen Request durch — die anderen 49 bekommen null zurück und scheitern.

TOCTOU — Time-of-Check vs. Time-of-Use

Eine spezifische Race-Condition-Variante: Time-of-Check to Time-of-Use. Anwendung prüft eine Bedingung, führt danach die Aktion aus. Zwischen Prüfung und Aktion ändert sich der Zustand.

Klassisches Beispiel: Konto-Überweisung.

JavaScript toctou-transfer.js
// Schadhaft: TOCTOU
async function transfer(fromId, toId, amount) {
  const account = await db.accounts.findOne({ id: fromId });

  if (account.balance < amount) {
    throw new Error('Insufficient funds');
  }

  // Zeitfenster: parallele Requests können hier zwischenliegen
  await sleep(0); // simuliert IO-Wartezeit

  await db.accounts.update({ id: fromId }, { $inc: { balance: -amount } });
  await db.accounts.update({ id: toId }, { $inc: { balance: amount } });
}

Mit zwei parallelen 100-EUR-Transfers von einem Konto mit 150 EUR Guthaben: beide Checks sehen 150 EUR > 100 EUR. Beide gehen durch. Konto wird auf -50 EUR.

Schutz-Patterns:

  • Atomare Operationen mit BedingungUPDATE accounts SET balance = balance - 100 WHERE id = ? AND balance >= 100 RETURNING balance — DB selbst prüft und ändert in einem Schritt.
  • Datenbank-Transaktionen mit Serializable Isolation — DB serialisiert parallele Transaktionen.
  • Optimistic Locking — Version-Spalte; Update schlägt fehl, wenn Version sich geändert hat.
  • Pessimistic LockingSELECT ... FOR UPDATE sperrt die Zeile bis Transaktion-Ende.

Workflow-Bypass

Multi-Step-Workflows (Checkout, Onboarding, Reset-Verfahren) bestehen aus mehreren Schritten, oft mit Validierung pro Schritt. Wenn die einzelnen Schritte unabhängig erreichbar sind und nicht serverseitig vorgeschrieben werden, lassen sich Schritte überspringen.

Klassische Beispiele:

  • Bezahl-Schritt überspringen. Checkout hat Schritte: Cart → Adresse → Bezahlung → Bestätigung. Wenn der „Bestätigung"-Endpunkt direkt aufrufbar ist, ohne dass der Bezahl-Schritt im Server-State markiert ist — Bestellung wird ohne Bezahlung ausgelöst.
  • MFA überspringen. Login-Endpunkt setzt Session-Cookie nach Passwort-Eingabe; MFA-Endpunkt prüft Code und „aktiviert" die Session. Wenn die Session schon vor MFA gültig ist, kann der MFA-Schritt übergangen werden, indem direkt auf andere Endpunkte gegangen wird.
  • E-Mail-Verifikations-Überspringung. Account hat verified: false nach Registrierung. Wenn sicherheitsrelevante Aktionen nicht alle auf verified: true prüfen, lassen sie sich vor Verifikation ausführen.
  • Free-Trial-Schritt überspringen. Onboarding-Workflow: 14-Tage-Trial → Kreditkarten-Eingabe → Vollzugang. Wenn der „Vollzugang"-Schritt nicht prüft, ob Kreditkarte hinterlegt ist — Endlos-Trial möglich.

Schutz-Patterns:

  • Server-seitiger State-Machine-Status — Workflow-State wird ausschließlich serverseitig gehalten. Jeder Schritt validiert, dass der vorherige Schritt abgeschlossen wurde.
  • Step-Tokens — jeder Schritt gibt ein einmalig gültiges Token zurück, das für den nächsten Schritt benötigt wird.
  • Sequenz-IDs mit serverseitiger Validierung — Reihenfolge ist nicht durch Client manipulierbar.
  • Default-Deny für unbestätigte Workflows.

Limit-Bypass

Wenn Anwendung Limits enforce — „nur 3 Aktivierungen pro Tag", „nur 1 Gutschein pro Konto" — gibt es oft kreative Umgehungs-Wege.

Klassische Beispiele:

  • Per-IP-Limit umgangen durch Tor / VPN / mobiles Netz.
  • Per-Account-Limit umgangen durch Massen-Account-Erstellung (Stichwort: Sneaker-Drops, Bonus-Aktionen).
  • Per-E-Mail-Limit umgangen durch Gmail-Punkte-Trick (alice@gmail.com, a.lice@gmail.com, al.ice@gmail.com sind dieselbe Adresse, werden aber als verschieden gespeichert).
  • Per-Karten-Limit umgangen durch verschiedene Karten desselben Nutzers.
  • Zeitliche Limits umgangen durch Manipulation der Server-Zeit-Annahmen.
  • Negative Werte für Mengen — Bestellung mit quantity: -5 führt zu Guthaben.

Schutz-Patterns:

  • Mehrschichtige Identifikation für Limits — IP + Account + Device-Fingerprint + Zahlungs-Methode kombiniert.
  • Server-seitige Werte-Validierung — keine negativen Werte, keine Werte außerhalb plausibler Bereiche.
  • CAPTCHA / Bot-Schutz vor Massen-Account-Erstellung.
  • Verzögerte Aktivierung und Hold-Times für Bonus-Auszahlungen.
  • E-Mail-Normalisierung vor Vergleichen (lowercase, Gmail-Punkte entfernen).

Authentifizierungs-/Reset-Workflows

Eine besonders sensitive Klasse — Password-Reset und Account-Recovery sind klassische Business-Logic-Fallen.

Konkrete Anti-Patterns:

  • Reset-Token nicht nach Nutzung invalidiert — User kann mit altem Token zurück.
  • Reset-Token nicht nach Passwort-Wechsel invalidiert — wenn parallel ein Reset offen war, kann der/die Angreifer:in das Passwort später nochmal ändern.
  • Session-Cookies bei Passwort-Wechsel nicht invalidiert — wenn Angreifer:in eine Session hatte, behält sie die Session auch nach dem User-Passwort-Wechsel.
  • Reset-Mail kann beliebig oft angefordert werden — Spam-Quelle, manchmal Account-Lockout-Vektor.
  • Reset-Workflow exposes ob ein Account existiert — Enumeration-Risiko.
  • OAuth-Account-Linking ohne erneute Authentifizierung — Angreifer:in mit OAuth-Provider-Zugang verknüpft eigenen Provider mit Opfer-Konto.

Schutz-Patterns:

  • Reset-Token: einmalig + kurz gültig (max. 1 Stunde) + invalidiert bei Nutzung + invalidiert bei Passwort-Wechsel über anderen Weg.
  • Alle Sessions terminieren bei Passwort-Wechsel oder MFA-Änderung.
  • Rate-Limit auf Reset-Anfrage (z. B. max 3 pro Tag pro Account).
  • Generische Antworten bei Reset-Anforderung (kein „Account existiert nicht").
  • Re-Auth bei kritischen Aktionen — Passwort-Wechsel, OAuth-Link, MFA-Removal verlangen erneute Passwort-Eingabe.

Numerische und State-Probleme

Eine Sammlung typischer Edge-Cases:

  • Integer-Overflow / Underflowquantity * price wird negativ bei extrem hohen Werten. Bei JavaScript-Number ab 2^53 interessante Effekte.
  • Floating-Point-Rundung — Geld in Float speichern (0.1 + 0.2 = 0.30000000000000004) führt zu Diskrepanzen.
  • Negative Zahlen wo positive erwartet wurden — quantity: -5 als „Rückbuchung".
  • Race auf Counter — zwei parallele +1-Operationen führen zu +1 statt +2, wenn nicht atomar.
  • State-Transitions nicht validiert — Bestellung kann von „shipped" zurück auf „pending" gesetzt werden, was Re-Bezahl ermöglichen würde.
  • Cancellation-Logic — Bestellung canceln führt zur Auszahlung, obwohl noch nicht bezahlt wurde.
  • Zeitliche Annahmen — Server-Zeitzone vs. Client-Zeitzone, Coupon „gültig bis 23:59" wird per Client-Zeit umgangen.

Schutz-Patterns:

  • Schema-Validierung mit strikten Typen (positive Integer, Min/Max-Range).
  • Currency-Library für Geld-Berechnungen (Dezimal-Typen statt Float).
  • State-Machine mit erlaubten Transitions; ungültige Transitions werfen Fehler.
  • Atomare Counter (UPDATE ... SET count = count + 1 statt count = ?).
  • Server-Zeit für alle Zeit-Vergleiche.

Wie man Business-Logic-Flaws findet

Drei Strategien — alle aufwendig, alle wertvoll:

1. Bedrohungs-Modellierung pro Workflow. Für jede sensible User-Story: „Was, wenn diese Aktion zweimal gleichzeitig ausgeführt wird?", „Was, wenn Schritt 3 vor Schritt 2 kommt?", „Was, wenn ein Wert außerhalb der erwarteten Range liegt?". Antworten dokumentieren und testen.

2. Property-Based Testing. Statt Beispiele zu testen, Eigenschaften der Anwendung deklarieren — und Tools (Hypothesis, fast-check, jqwik) generieren Tausende Test-Variationen. Findet Edge-Cases, die manuelle Tests verpassen.

3. Adversarial-Modus im Test-Team. Ein Test-Mitglied übernimmt explizit die Rolle des „bösen Nutzers": versucht doppelt einzulösen, Workflow zu überspringen, Limits zu umgehen. Sehr produktiver Pentest-Modus, der oft mehr Befunde liefert als Standard-Scanner.

4. Bug Bounty als laufende Quelle. Forschende finden Business-Logic-Flaws regelmäßig — sie haben das Verständnis, das automatische Tools nicht haben. HackerOne- und Bugcrowd-Reports sind voll von kreativen Workflow-Bypass- und Race-Condition-Funden.

Werkzeuge für Race-Condition-Tests

Konkrete Werkzeuge, mit denen Forschende parallele Anfragen erzeugen:

  • Burp Suite Repeater + Send Group in Parallel — Burp Suite Pro kann Anfragen-Gruppen in Single-Packet-Attacks senden, sodass sie nahezu zeitgleich am Server ankommen.
  • Turbo Intruder (Burp-Erweiterung von James Kettle) — extrem schnelles parallel Senden mit kontrollierbarer Konkurrenz.
  • ab / wrk / hey — klassische HTTP-Last-Test-Tools, auch für Race-Tests nutzbar.
  • xargs -P + curl — niedrigschwellig: parallele Curl-Aufrufe per xargs.

Für Bug-Bounty-Berichte ist die Reproduzierbarkeit wichtig: ein kurzes Python- oder Curl-Script, das die Race-Condition zuverlässig auslöst, macht den Bericht wertvoller.

Häufige Stolperfallen

Race Conditions sind 2023/24 stark gewachsen

Mit dem Aufkommen von HTTP/2-Multiplexing und Single-Packet-Attack-Techniken (James Kettle, PortSwigger Research 2023) sind Race Conditions in der Bug-Bounty-Welt zu einem der attraktivsten Funde-Themen geworden. Anfragen lassen sich in Mikrosekunden-Fenstern parallelisieren, was viele Schutz-Annahmen bricht.

DB-Transaktionen lösen nicht alles

Eine Datenbank-Transaktion mit Default-Isolation-Level (Read Committed) hilft nicht zwingend gegen Race Conditions. Du brauchst entweder atomare Update-mit-Bedingung, Serializable Isolation oder explizites SELECT FOR UPDATE. Ein BEGIN; SELECT; UPDATE; COMMIT; ohne Locking ist nicht race-condition-sicher.

Idempotenz als Schutz

Eine Anfrage ist idempotent, wenn mehrfache Ausführung dasselbe Ergebnis hat wie einmalige. Klassisches Pattern: Client schickt einen Idempotency-Key mit jeder Anfrage. Server speichert die Antwort beim ersten Aufruf; bei weiteren Aufrufen mit demselben Key wird die gespeicherte Antwort zurückgegeben, ohne die Aktion erneut auszuführen. Stripe, PayPal nutzen das produktiv.

Bug Bounty bezahlt für Logic-Flaws gut

In den meisten Bug-Bounty-Programmen sind Business-Logic-Flaws unter den teuersten Funden — schwer zu finden, oft hoher Wirkung, oft monetäre Auswirkung (Geld-Abfluss, Free-Service-Missbrauch). Hacker-Reports gehen häufig im fünfstelligen Dollar-Bereich.

Workflow-Diagramm als Bedrohungs-Analyse-Tool

Wer einen Workflow sichern will, sollte ihn als State-Machine-Diagramm aufmalen — alle erlaubten Übergänge, alle nicht erlaubten Übergänge. Anschließend für jeden Übergang fragen: „Was, wenn ein:e Angreifer:in versucht, diesen Übergang aus einem anderen State zu erzwingen?" — die Antworten sind die Test-Cases.

Gmail-Punkte-Trick als E-Mail-Normalisierung

Gmail behandelt alice@gmail.com, a.lice@gmail.com und al.ice+test@gmail.com alle als dieselbe Adresse — Zeichen werden ignoriert. Wer User-Anzahl per E-Mail-Adresse limitiert, sollte die Adresse normalisieren (lowercase, Punkte entfernen, Plus-Tags ignorieren), bevor man sie als Identifier verwendet.

Single-Packet-Attack von James Kettle

Eine Forschung von 2023: durch HTTP/2-Multiplexing lassen sich mehrere HTTP-Anfragen in einem einzigen TCP-Paket bündeln. Damit kommen sie nahezu gleichzeitig beim Server an — Race-Condition-Fenster sind dadurch von Millisekunden auf Mikrosekunden geschrumpft. Manche Schutz-Mechanismen, die auf „kommt-bestimmt-nicht-gleichzeitig" setzten, sind seitdem brüchig.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu OWASP Top 10

Zur Übersicht