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

Bezpieczeństwo webhooków: podpisy, replay i SSRF

Webhooki mają dwie strony i dwie powierzchnie ataku. Jak weryfikować podpisy, blokować replay, projektować idempotencję i nie zbudować SSRF na życzenie.

MATERIAŁ PUBLICZNY
AUTOR
/ CEO Breachroad · OSCP · PNPT
PUBLIKACJA
25 lipca 2026
CZAS CZYTANIA
18 min czytania
TEMAT
Pentest i AppSec
Bezpieczeństwo webhooków: podpisy, replay i SSRF

Webhook jest jedynym endpointem, który świadomie zapraszasz obcy system do wywoływania — i jednocześnie jedyną funkcją, w której twój serwer na życzenie użytkownika wykonuje żądanie pod dowolny adres. To dwie zupełnie różne powierzchnie ataku, a większość materiałów na ten temat je miesza, przez co część zaleceń trafia w niewłaściwą stronę.

Ten artykuł rozdziela role. Najpierw odbieranie webhooków, czyli sytuacja, w której twoja aplikacja jest konsumentem zdarzeń z zewnętrznego systemu. Potem wysyłanie, czyli sytuacja, w której twoja aplikacja pozwala klientom zarejestrować adres i sama wykonuje do niego żądania.

Część pierwsza: odbieranie webhooków

Podpis liczony z surowego ciała

Podstawowym mechanizmem uwierzytelnienia nadawcy jest podpis, zwykle kod uwierzytelniający wiadomość liczony wspólnym sekretem, przesyłany w nagłówku. Kluczowy szczegół implementacyjny brzmi: podpis weryfikujemy na surowych bajtach ciała żądania, dokładnie takich, jakie przyszły.

Najczęstszy błąd polega na tym, że framework parsuje ciało do struktury danych, aplikacja serializuje ją z powrotem i liczy podpis z wyniku. Wystarczy inna kolejność kluczy, inne formatowanie liczb albo znak białego miejsca, żeby wynik przestał się zgadzać — a jeśli akurat się zgadza, to znaczy tylko tyle, że działa dla obecnej wersji biblioteki. W wielu szkieletach aplikacyjnych dostęp do surowego ciała wymaga świadomej konfiguracji, bo domyślny strumień jest konsumowany przez parser.

Drugi szczegół to porównanie stałoczasowe. Zwykłe porównanie napisów kończy się na pierwszej różnicy i teoretycznie ujawnia informację o poprawnym prefiksie. Koszt użycia właściwej funkcji porównującej jest zerowy, więc nie ma powodu, żeby tego nie robić.

Trzeci to klucz per nadawca i wersjonowanie. Nagłówek podpisu powinien nieść identyfikator wersji schematu, tak aby rotacja sekretu i zmiana algorytmu były możliwe bez przestoju: przez pewien czas akceptujemy dwa podpisy, potem wycofujemy stary.

Replay i okno czasowe

Poprawny podpis nie oznacza, że wiadomość jest świeża. Żądanie przechwycone lub odtworzone z logów pozostaje poprawne bezterminowo, jeżeli podpis obejmuje wyłącznie treść.

Rozwiązanie ma dwa elementy. Po pierwsze, znacznik czasu objęty podpisem i odrzucanie żądań spoza wąskiego okna tolerancji, liczonego w minutach. Po drugie, rejestr identyfikatorów zdarzeń przechowywany co najmniej tak długo, jak wynosi okno tolerancji, żeby to samo zdarzenie nie zostało przetworzone dwa razy.

Idempotencja jest wymogiem, nie optymalizacją

Praktycznie każdy dostawca gwarantuje dostarczenie co najmniej raz, co oznacza, że duplikaty są normalnym stanem pracy, a nie awarią. Ponowna próba po przekroczeniu limitu czasu, ponowienie po błędzie sieci, ręczne wznowienie kolejki — wszystko to generuje powtórzenia.

Konsument musi więc być idempotentny: identyfikator zdarzenia zapisywany w tej samej transakcji co skutek biznesowy, a przy powtórzeniu operacja pomijana. Bez tego zdarzenie „obciąż konto” bywa przetwarzane dwukrotnie i dyskusja przenosi się z bezpieczeństwa na reklamacje.

Warto też pamiętać, że kolejność dostarczenia nie jest gwarantowana. Aktualizacja starsza może przyjść po nowszej. Odporność na to buduje się przez numer wersji obiektu albo znacznik czasu zdarzenia, a nie przez założenie, że kolejka jest uporządkowana.

Zaufanie do treści zdarzenia

Nawet poprawnie podpisane zdarzenie mówi tylko tyle, że nadawca je wysłał. Dla operacji o skutkach finansowych albo zmieniających uprawnienia bezpieczniejszy jest wzorzec cienkiego ładunku: zdarzenie niesie identyfikator i typ, a aplikacja sama odpytuje interfejs dostawcy o aktualny stan obiektu. Dzięki temu nie trzeba ufać treści przesyłki, a przy okazji wrażliwe dane nie krążą w ciele żądania i nie lądują w logach.

To także rozwiązuje problem zdarzeń przeterminowanych: pobrany stan jest zawsze aktualny, niezależnie od tego, kiedy zdarzenie dotarło.

Warstwy dodatkowe i ich granice

Ograniczenie adresów źródłowych do zakresów dostawcy bywa użyteczne, ale rzadko wystarcza: dostawcy zmieniają zakresy, korzystają z sieci dostarczania treści, a ruch bywa zestawiany przez pośredników. Traktuj to jako warstwę dodatkową, nigdy jako zamiennik podpisu.

Uwierzytelnianie wzajemne certyfikatami jest mocniejsze niż wspólny sekret i warto je rozważyć w integracjach o wysokiej wartości, choć koszt operacyjny jest wyższy.

Sekret w adresie URL webhooka — czyli „nikt nie zna tej ścieżki” — nie jest uwierzytelnieniem. Adresy trafiają do logów pośredników, do systemów monitorowania i do zgłoszeń serwisowych.

Odporność na przeciążenie

Endpoint webhooka jest publiczny, więc obowiązują go zwykłe zasady: limit rozmiaru ciała, limit tempa, twardy limit czasu przetwarzania. Dobrą praktyką jest szybka odpowiedź i przetwarzanie asynchroniczne: zweryfikuj podpis, zapisz zdarzenie do kolejki, odpowiedz kodem powodzenia, a logikę wykonaj poza cyklem żądania. To skraca czas odpowiedzi, zmniejsza liczbę ponowień po stronie dostawcy i ogranicza skutki kosztownej operacji.

Uważaj też na logi. Pełne ciało zdarzenia bywa zapisywane „na potrzeby diagnostyki” i zawiera dane osobowe albo szczegóły płatności. Podpis i nagłówki uwierzytelniające powinny być redagowane domyślnie.

Część druga: wysyłanie webhooków

Tu sytuacja się odwraca. Klient rejestruje adres, a twój serwer wykonuje do niego żądania. To jest SSRF z założenia — funkcja polega dokładnie na tym, żeby serwer połączył się z adresem podanym przez użytkownika. Cała sztuka polega na tym, żeby ta funkcja nie sięgała tam, gdzie nie powinna.

Ochrona przed dostępem do sieci wewnętrznej

Walidacja samego napisu z adresem nie wystarcza. Konieczne jest rozwiązanie nazwy i sprawdzenie adresu docelowego przed nawiązaniem połączenia: odrzucenie pętli zwrotnej, zakresów prywatnych, adresów lokalnych łącza — w tym adresu usługi metadanych w chmurze — zakresów specjalnego przeznaczenia oraz odpowiedników w IPv6, łącznie z adresami odwzorowanymi z IPv4.

Następnie pojawia się problem, o którym łatwo zapomnieć: między sprawdzeniem a połączeniem nazwa może zostać rozwiązana ponownie, na inny adres. To jest rebinding DNS, a odporność na niego wymaga albo połączenia z konkretnym, wcześniej zweryfikowanym adresem, albo ponownej walidacji na poziomie gniazda.

Przekierowania są kolejną furtką: adres zewnętrzny odpowiada przekierowaniem do adresu wewnętrznego. Bezpiecznie jest nie podążać za przekierowaniami w ogóle, a jeżeli to niemożliwe — walidować każdy kolejny adres tak samo jak pierwszy i ograniczyć liczbę przeskoków.

Warstwą, która najskuteczniej domyka temat, jest dedykowane wyjście sieciowe: ruch webhooków wychodzi przez oddzielny komponent, który nie ma dostępu do sieci wewnętrznej, nie ma poświadczeń chmurowych i nie widzi usługi metadanych. Wtedy błąd w walidacji przestaje być krytyczny.

Do tego dochodzą oczywiste ograniczenia: tylko schemat HTTPS, tylko standardowy port, brak obsługi schematów innych niż sieciowe, twardy limit czasu, limit rozmiaru odpowiedzi i brak przekazywania treści odpowiedzi użytkownikowi. Ta ostatnia zasada jest ważna, bo zamienia ewentualny SSRF w wariant ślepy.

Weryfikacja własności adresu

Przy rejestracji adresu warto wykonać próbę potwierdzającą, że rejestrujący kontroluje ten adres: wysłać zdarzenie kontrolne z wartością losową i oczekiwać jej odesłania albo poprosić o umieszczenie potwierdzenia w odpowiedzi. Bez tego twoja infrastruktura może zostać użyta do generowania ruchu w kierunku cudzych systemów, a przy odpowiedniej skali — do ataku odmowy usługi cudzymi rękami.

Podpisywanie tego, co wysyłasz

Skoro oczekujesz podpisów od dostawców, podpisuj również własne zdarzenia. Minimalny standard to kod uwierzytelniający liczony z surowego ciała, znacznik czasu objęty podpisem, identyfikator zdarzenia i wersja schematu podpisu. Opublikuj instrukcję weryfikacji w dokumentacji i przewidź rotację sekretu z oknem, w którym ważne są dwa klucze.

Standaryzacja też ma znaczenie. Specyfikacja podpisów wiadomości HTTP opisuje mechanizm niezależny od dostawcy, a inicjatywy standaryzujące format webhooków ułatwiają konsumentom poprawną implementację. Im mniej autorskich schematów, tym mniej błędów po stronie odbiorców.

Ponawianie i wygaszanie

Ponawianie powinno mieć rosnący odstęp, ograniczoną liczbę prób i limit czasu życia zdarzenia. Endpoint, który przez dłuższy czas zwraca błędy, powinien zostać automatycznie wyłączony z powiadomieniem właściciela — inaczej twój system będzie miesiącami uderzał w cudzy serwer.

Warto również ograniczać równoległość na pojedynczego odbiorcę. Bez tego pojedyncza integracja potrafi wygenerować ruch, który dla odbiorcy jest nieodróżnialny od ataku.

Metodyka testu

Po stronie odbierania sprawdź kolejno: czy żądanie bez podpisu jest odrzucane; czy podpis policzony z innego sekretu jest odrzucany; czy modyfikacja pojedynczego bajtu ciała unieważnia podpis; czy żądanie sprzed godziny jest odrzucane; czy powtórzenie tego samego zdarzenia wywołuje skutek biznesowy dwa razy; czy zmiana kolejności pól w ciele wpływa na wynik weryfikacji; czy przetwarzanie odbywa się przed odpowiedzią i jak zachowuje się przy dużym ciele.

Po stronie wysyłania sprawdź, czy rejestracja adresu wskazującego na pętlę zwrotną, adres prywatny lub usługę metadanych zostaje odrzucona; czy nazwa domenowa wskazująca na adres wewnętrzny jest blokowana; czy przekierowanie do adresu wewnętrznego jest zatrzymywane; czy treść odpowiedzi odbiorcy jest ujawniana użytkownikowi; czy istnieją limity czasu, rozmiaru i liczby prób; czy adres jest weryfikowany przy rejestracji.

Wszystkie próby wykonuj we własnym środowisku testowym lub w zakresie objętym zgodą — testowanie wychodzącego SSRF na produkcji potrafi wywołać alerty i incydent po stronie sieci.

Detekcja

Po stronie odbierania monitoruj liczbę odrzuconych podpisów i odrzuceń z powodu przeterminowania. Nagły wzrost tych pierwszych oznacza albo błędną rotację sekretu, albo próbę podszycia. Monitoruj też liczbę duplikatów — jej wzrost może wskazywać na odtwarzanie starych żądań.

Po stronie wysyłania najcenniejszym sygnałem są próby połączeń do zakresów prywatnych i do adresu usługi metadanych. W poprawnie zaprojektowanym systemie takie zdarzenie nie powinno wystąpić nigdy, więc każde jest alertem. Warto również obserwować odbiorców z nietypowo wysokim wskaźnikiem błędów oraz adresy rejestrowane masowo z jednego konta.

Wersjonowanie zdarzeń i zmiany schematu

Webhooki są kontraktem, który trudniej zmienić niż interfejs odpytywany przez klienta, ponieważ to ty inicjujesz połączenie i nie wiesz, jak odbiorca sparsuje treść. Zmiana, która w API byłaby drobna, tutaj potrafi wyłączyć integracje dziesiątkom klientów naraz.

Zasady, które to porządkują, są proste. Typ zdarzenia i wersja schematu są jawne w ładunku i w nagłówku, więc odbiorca może rozgałęzić logikę. Dodawanie pól jest zmianą zgodną wstecz i wymaga od konsumentów jedynie ignorowania nieznanych kluczy — warto to wprost napisać w dokumentacji. Usunięcie pola, zmiana typu wartości albo zmiana znaczenia pola jest zmianą niezgodną i wymaga nowej wersji zdarzenia oraz okresu, w którym wysyłane są obie.

Warto również pozwolić odbiorcy wybrać, które typy zdarzeń chce otrzymywać. To nie jest wyłącznie wygoda: subskrypcja zawężona do potrzebnych typów zmniejsza ilość danych opuszczających twój system i ogranicza skutki błędnie skonfigurowanego adresu.

Osobną decyzją jest zawartość ładunku. Ładunek pełny jest wygodny, ale oznacza, że dane biznesowe trafiają pod adres kontrolowany przez klienta i pozostają w logach po drodze. Ładunek cienki — identyfikator, typ, znacznik czasu — wymaga od odbiorcy dodatkowego zapytania, lecz znacząco zmniejsza powierzchnię wycieku i ułatwia zachowanie zgodności, bo schemat pozostaje stabilny.

Checklista

Podpis jest liczony z surowych bajtów ciała i porównywany stałoczasowo. Podpis obejmuje znacznik czasu, a żądania spoza okna tolerancji są odrzucane. Identyfikatory zdarzeń są zapisywane, a przetwarzanie jest idempotentne. Kolejność zdarzeń nie jest zakładana. Operacje o skutkach finansowych potwierdzane są odpytaniem źródła. Endpoint ma limity rozmiaru, tempa i czasu, a przetwarzanie jest asynchroniczne. Ciało zdarzenia i nagłówki uwierzytelniające są redagowane w logach. Adresy odbiorców są walidowane po rozwiązaniu nazwy, z blokadą zakresów wewnętrznych. Przekierowania są zablokowane lub walidowane ponownie. Ruch wychodzący idzie przez wydzielone wyjście bez poświadczeń. Treść odpowiedzi odbiorcy nie wraca do użytkownika. Rejestracja adresu wymaga potwierdzenia własności. Wysyłane zdarzenia są podpisywane, a sekrety rotowane z oknem.

Wniosek

Webhooki wyglądają na prosty mechanizm — żądanie HTTP w drugą stronę — i właśnie dlatego bywają wdrażane bez modelu zagrożeń. Tymczasem po stronie odbioru mają wszystkie problemy uwierzytelniania wiadomości, a po stronie wysyłki są fabrycznie zbudowanym SSRF, który trzeba świadomie ograniczyć.

Dwie decyzje dają najwięcej: weryfikacja podpisu z surowego ciała razem ze znacznikiem czasu i rejestrem zdarzeń oraz wydzielone wyjście sieciowe dla ruchu wychodzącego. Pierwsza zamyka podszycie i odtwarzanie, druga sprawia, że nawet błąd w walidacji adresu nie prowadzi do wnętrza infrastruktury.


Źródła pierwotne: RFC 9421 — HTTP Message Signatures, OWASP — Server Side Request Forgery Prevention Cheat Sheet, Standard Webhooks.

UDOSTĘPNIJ / KOPIUJ