Przejdź do treści
INDEKS ANALIZ BREACHROAD / NOTA TECHNICZNA

Race condition w aplikacjach: testy TOCTOU

Race condition i TOCTOU w aplikacjach webowych: poznaj bezpieczne testy współbieżności, wiarygodne dowody oraz skuteczne wzorce naprawcze.

MATERIAŁ PUBLICZNY
AUTOR
/ Pentester (OSCP, PNPT)
PUBLIKACJA
4 maja 2026
CZAS CZYTANIA
16 min czytania
TEMAT
AppSec
Race condition w aplikacjach: testy TOCTOU

Race condition w aplikacji webowej powstaje wtedy, gdy wynik operacji zależy od kolejności lub czasu wykonania kilku równoległych żądań, a system nie chroni spójności współdzielonego stanu. Dwa poprawne osobno żądania mogą wejść w krytyczną sekcję jednocześnie i razem utworzyć stan, którego projektant nie przewidział: dwukrotnie wykorzystany kupon, przekroczony limit wypłaty, kilka resetów tego samego hasła albo konto aktywowane przed zakończeniem weryfikacji.

Najkrótsza odpowiedź: test race condition nie polega na bezmyślnym wysłaniu setek żądań. Najpierw modeluje się maszynę stanów i niezmienniki biznesowe, potem porównuje wykonanie sekwencyjne z kontrolowaną próbą równoległą, a na końcu potwierdza skutek na danych testowych. Naprawa musi atomowo połączyć sprawdzenie warunku ze zmianą stanu — sam rate limiting zwykle nie usuwa przyczyny.

MITRE klasyfikuje problem jako CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization. Konsekwencje obejmują naruszenie integralności, obejście kontroli dostępu, eskalację uprawnień, ujawnienie danych i niedostępność. W aplikacjach internetowych luka często wygląda niepozornie, bo pojedynczy request przechodzi wszystkie walidacje. Błąd ujawnia się dopiero w relacji między requestami.

TOCTOU, race window i utracona aktualizacja

Klasyczny wariant time-of-check to time-of-use, czyli TOCTOU, rozdziela decyzję od wykonania. Aplikacja sprawdza, czy kupon nie został użyty, następnie oblicza rabat i dopiero potem zapisuje użycie. Jeżeli dwa procesy zakończą sprawdzenie przed pierwszym zapisem, oba uznają operację za dozwoloną.

Żądanie A: odczyt used=false ── obliczenie ── zapis used=true
Żądanie B:      odczyt used=false ── obliczenie ── zapis used=true
                         ^ race window ^

Okno wyścigu może trwać milisekundy, lecz jego długość nie decyduje o wadze błędu. Atakujący nie musi znać wewnętrznego harmonogramu. Powtarza kontrolowaną serię równoległych operacji i obserwuje niezmienniki: saldo nie może spaść poniżej zera, token jednorazowy nie może zostać zaakceptowany dwa razy, a zamówienie nie może przejść do paid, jeżeli kwota po autoryzacji się zmieniła.

W praktyce warto rozróżnić kilka klas:

KlasaPrzykładNaruszony niezmiennik
limit overrunwielokrotne użycie kuponu lub darmowego trialalicznik użyć ≤ limit
lost updatedwa zapisy salda oparte na tym samym odczycieżadna zatwierdzona zmiana nie ginie
multi-endpoint racepłatność i zmiana koszyka wykonane równolegleopłacona kwota odpowiada zawartości
partial constructionużycie konta pomiędzy utworzeniem a weryfikacjąkonto nie ma uprawnień przed aktywacją
double-spenddwie wypłaty korzystają z tego samego saldasuma rezerwacji nie przekracza środków

Badania PortSwigger „Smashing the state machine” pokazały, że współczesne race conditions wykraczają poza powielanie kuponów. Ważne są ukryte podstany, cache sesji, kolejki, asynchroniczne workery oraz kilka endpointów zmieniających jeden obiekt.

Gdzie szukać race condition

Najlepszym punktem startu nie jest lista payloadów, tylko modelowanie zagrożeń i mapa operacji nieodwracalnych. Kandydatami są:

  • płatności, zwroty, wypłaty, rezerwacje i naliczanie punktów;
  • aktywacja konta, reset hasła, zmiana e-maila i akceptacja zaproszeń;
  • limity API, próby MFA, kody jednorazowe i mechanizmy anty-bruteforce;
  • tworzenie zasobów o unikalnej nazwie lub przypisywanie ról;
  • importy plików, generowanie raportów i zadania kolejkowane;
  • operacje check → update, szczególnie rozdzielone między usługi.

W mikroserwisach problem może nie wystąpić w jednej bazie. Serwis zamówień rezerwuje towar, płatności potwierdzają transakcję, a worker wysyła realizację. Każdy komponent lokalnie działa prawidłowo, lecz komunikaty mogą przyjść ponownie, poza kolejnością albo po timeoutcie klienta. Dlatego bezpieczeństwo API musi obejmować semantykę ponowień, idempotency keys i stan procesów asynchronicznych.

Bezpieczna metodyka testu penetracyjnego

Test wykonuj tylko w uzgodnionym środowisku i na kontach należących do zespołu testowego. Współbieżność potrafi utworzyć trudne do cofnięcia skutki: kilka płatności, wiele wiadomości e-mail, podwójne zadania lub niespójne rekordy. Zakres powinien określać maksymalną liczbę requestów, dozwolone obiekty oraz sposób sprzątania.

1. Zapisz niezmiennik

„Kod można użyć raz” jest lepszą hipotezą niż „endpoint może być podatny”. Zapisz stan początkowy, oczekiwany wynik jednej operacji i maksymalny dopuszczalny wynik serii. Dla transferu będą to dwa salda, identyfikator transakcji i suma księgowa; dla zaproszenia — liczba użyć, rola i czas wygaśnięcia.

2. Ustal baseline sekwencyjny

Wyślij żądania jedno po drugim. Zanotuj statusy HTTP, treść, czas, zmiany w interfejsie i efekty drugiego rzędu: e-mail, wpis audytowy, rekord w historii czy pracę kolejki. Jeśli aplikacja zachowuje się niestabilnie już sekwencyjnie, wynik próby równoległej będzie trudny do interpretacji.

3. Zminimalizuj różnice czasowe

Web Security Academy opisuje dla HTTP/1 technikę synchronizacji ostatniego bajtu, a dla HTTP/2 podejście single-packet, które ogranicza jitter sieciowy. To narzędzia diagnostyczne, nie dowód same w sobie. Równoległe wysłanie requestów ma jedynie zwiększyć szansę, że backend przetworzy je w tym samym oknie.

4. Porównaj stan, nie tylko odpowiedzi

Dwie odpowiedzi 200 OK mogą oznaczać fałszywie pozytywny wynik, jeżeli tylko jedna transakcja została zatwierdzona. Odwrotnie, jedna odpowiedź może informować o błędzie, mimo że worker wykonał oba zadania. Dowodem jest trwały stan: dwa wykorzystania tego samego tokenu, ujemne saldo, nadmiarowy zasób lub podwójny wpis księgowy.

5. Zredukuj próbę

Po znalezieniu anomalii zmniejsz liczbę requestów, odtwórz warunki i ustal minimalną parę operacji. Oddziel wpływ cache, sesji, różnych połączeń i regionów. Nie eskaluj do realnej szkody; kontrolowany dowód naruszenia niezmiennika wystarcza do raportu.

Profesjonalny pentest aplikacji webowej opisuje także częstotliwość reprodukcji. Wyścig występujący w dwóch z pięćdziesięciu prób nadal może być krytyczny, ale zespół naprawczy musi wiedzieć, jak odróżnić błąd od szumu infrastruktury.

Dlaczego typowe „naprawy” zawodzą

Rate limiting

Limit requestów zmniejsza tempo prób, lecz dwa żądania nadal mogą wejść w jedno okno. Mechanizm bywa rozproszony między węzłami, oparty na opóźnionym liczniku albo możliwy do obejścia przez kilka sesji. To warstwa ograniczająca nadużycia, nie substytut atomowości.

Flaga processing=true

Jeżeli kod najpierw odczytuje flagę, a potem ją ustawia, sama flaga ma własny race condition. Musi być ustawiana warunkowo w operacji atomowej, na przykład przez update z warunkiem lub blokadę wspieraną przez magazyn stanu.

Blokada tylko w pamięci procesu

Mutex w jednej instancji nie chroni przed drugim podem, workerem ani regionem. W architekturze skalowanej trzeba wskazać, gdzie naprawdę znajduje się wspólny stan i która warstwa gwarantuje serializację.

Dłuższa transakcja bez właściwej izolacji

Transakcja nie zawsze oznacza brak wyścigu. Poziom izolacji i konkretne zapytania decydują, czy dwa procesy zobaczą ten sam stan. Dokumentacja PostgreSQL opisuje zachowanie poziomów izolacji i możliwość błędów serializacji, które aplikacja powinna umieć ponowić: Transaction Isolation.

Skuteczne wzorce naprawcze

Pierwszym wyborem są niezmienniki wymuszone możliwie blisko danych. Unikalny indeks może zagwarantować, że token ma tylko jeden rekord użycia. Warunkowy update UPDATE ... WHERE used = false pozwala sprawdzić liczbę zmienionych wierszy bez osobnego odczytu. Dla sald stosuje się odpowiednie blokady rekordów, kontrolę wersji albo poziom SERIALIZABLE, uwzględniając retry.

Idempotency key wiąże logiczną operację z niepowtarzalnym identyfikatorem. Serwer zapisuje wynik pierwszego przetworzenia i dla ponowienia zwraca ten sam rezultat, zamiast wykonywać efekt jeszcze raz. Klucz musi być powiązany z użytkownikiem, parametrami i czasem życia; samo przyjęcie dowolnego nagłówka bez trwałej deduplikacji niczego nie gwarantuje.

W procesach rozproszonych potrzebne są jawne maszyny stanów. Dozwolone przejścia, na przykład created → authorized → captured, powinny być warunkowe i monotoniczne. Konsument kolejki musi tolerować duplikaty. Wzorce inbox/outbox pomagają powiązać zmianę bazy z publikacją komunikatu, ale również wymagają jednoznacznego klucza i obsługi ponowień.

ProblemPreferowana kontrolaTest regresyjny
jednorazowy tokenunikalność + atomowe zużycierównoległe użycie jednego tokenu
limit zasobuwarunkowy update/licznik w jednej transakcjiN+1 żądań przy limicie N
podwójna płatnośćidempotency key + księgaponowienie po timeoutcie
przejście stanucompare-and-swap / wersjonowaniedwa sprzeczne przejścia
duplikat z kolejkiidempotentny konsumentdwukrotne dostarczenie komunikatu

Naprawę trzeba objąć testem współbieżnym w CI. Zwykły test jednostkowy wykonywany sekwencyjnie może nigdy nie wejść w okno wyścigu. Przydatne są bariery synchronizacyjne w testach integracyjnych, kontrolowane opóźnienia między odczytem a zapisem oraz asercje na końcowym stanie bazy.

Jak raportować race condition

Raport powinien zawierać naruszony niezmiennik, stan początkowy, dwie minimalne operacje, liczbę prób, trwały efekt oraz granice testu. Dołącz identyfikatory requestów i rekordów, ale usuń tokeny sesyjne i dane osobowe. Zamiast ogólnego „brak blokady” opisz, gdzie rozdzielono decyzję od zapisu i która kontrola powinna zapewniać atomowość.

Wartość biznesową pokaż na realistycznym, lecz bezpiecznym scenariuszu: możliwość wielokrotnej realizacji rabatu oznacza stratę finansową; wyścig w resetowaniu hasła może oznaczać przejęcie konta; podwójne nadanie uprawnień narusza model autoryzacji. Priorytet powinien uwzględniać łatwość powtórzenia, skalę automatyzacji i możliwość wykrycia w logach.

Kolejki, webhooki i wyścigi poza HTTP

Brak anomalii przy równoległych requestach nie kończy testu, jeżeli efekt jest wykonywany asynchronicznie. Gateway może poprawnie odrzucić duplikat, ale webhook płatniczy zostanie dostarczony ponownie, konsument utraci zapis deduplikacji albo dwa workery pobiorą to samo zadanie po wygaśnięciu lease. Testuj ponowne dostarczenie identycznego komunikatu, zmianę kolejności oraz retry po timeoutcie między efektem a potwierdzeniem odbioru.

Identyfikator wiadomości powinien być przechowywany razem z wynikiem operacji, a nie tylko w krótkotrwałym cache. Telemetria musi łączyć request klienta, komunikat, próbę workera i finalny rekord domenowy. Inaczej zespół zobaczy dwa poprawne logi techniczne, lecz nie zauważy podwójnego skutku biznesowego.

OWASP ASVS 5.0 pomaga zamienić pojedynczą poprawkę w wymaganie dla całego SDLC, a porównanie SAST, DAST i IAST pokazuje, dlaczego żadne pojedyncze narzędzie nie wykryje wszystkich wyścigów. Skan statyczny może wskazać sekwencję check-then-act, lecz dopiero test na realnym magazynie stanu potwierdza zachowanie systemu.

Checklista dla zespołu

  • Czy każda operacja finansowa i jednorazowa ma zapisany niezmiennik?
  • Czy sprawdzenie warunku i zapis są atomowe w tej samej warstwie danych?
  • Czy unikalność jest wymuszana przez bazę, a nie wyłącznie przez kod aplikacji?
  • Czy retry klienta, gatewaya i kolejki jest bezpieczne?
  • Czy idempotency key jest trwały, związany z parametrami i odporny na kolizje?
  • Czy maszyna stanów odrzuca przejścia wstecz i sprzeczne aktualizacje?
  • Czy testy regresyjne naprawdę wykonują operacje równolegle?
  • Czy monitoring wykrywa ujemne salda, wielokrotne użycia i duplikaty efektów?

Race condition jest luką w logice i architekturze, nie problemem szybkości sieci. Jeżeli krytyczna operacja opiera się na założeniu „drugie żądanie nie zdąży”, założenie jest już błędem bezpieczeństwa. Potrzebujesz weryfikacji procesów płatności, kont lub API? Zamów test penetracyjny aplikacji webowej — sprawdzimy współbieżność na kontrolowanych danych i dostarczymy reprodukowalny test regresyjny.

UDOSTĘPNIJ / KOPIUJ