Zanim endpoint GraphQL API stanie się publiczny, uruchom te pięć testów na środowisku staging. Skonfiguruj staging tak, jak będzie skonfigurowana produkcja, bo w przeciwnym razie wyniki niewiele znaczą.
Testy odpowiadają czterem obszarom z OWASP GraphQL Cheat Sheet: walidacji danych wejściowych, kosztownym zapytaniom, kontroli dostępu i niebezpiecznej konfiguracji domyślnej.
1. Sprawdź ustawienia domyślne
Nadmiarowe komunikaty błędów, introspekcja i GraphiQL to typowe niebezpieczne ustawienia domyślne. Przetestuj wszystkie trzy. Zacznij od najmniejszego zapytania introspekcyjnego:
{__schema{types{name}}}
Zobacz, czy serwer odpowiada szczegółami schematu, czy odmawia. Potem poszukaj GraphiQL w przeglądarce. Na koniec wyślij zapytanie z błędną składnią oraz zapytanie z nieprawidłowym argumentem i przeczytaj komunikat błędu. Nazwy resolverów, stack trace'y lub komunikaty bazy danych w odpowiedzi to dla atakującego gotowa mapa.
Poprawka to kwestia konfiguracji. Wyłącz introspekcję i GraphiQL tam, gdzie API nie ma być eksplorowane, zwracaj ogólne komunikaty błędów, a szczegóły zostawiaj w logach po stronie serwera.
2. Przepuść wrogie dane przez każdy argument
Identyfikatory podaje użytkownik, a następnie data fetchery wykorzystują je w żądaniach HTTP, zapytaniach do bazy i innych wywołaniach. Właśnie tam dochodzi do injection. Przetestuj każdy argument każdego query i mutation payloadami, których użyłbyś w dowolnym innym API: SQL injection i NoSQL injection, OS command injection, SSRF oraz CRLF injection.
Żeby zautomatyzować ten test, wygeneruj listy argumentów ze schematu, dopóki introspekcja jest na stagingu włączona, przepuść przez nie swoje payloady, a potem wyłącz introspekcję.
Co wymusić:
- Konkretne typy GraphQL, takie jak skalary i enumy, oraz zdefiniowane schematy dla danych wejściowych mutacji.
- Allowlistę znaków, a nie denylistę. Dobrym punktem wyjścia są dane alfanumeryczne, bez znaków spoza ASCII (non-unicode), bo wykluczają wiele ataków.
- Zapytania parametryzowane dla wszystkiego, co trafia do interpretera. Jeśli używasz ORM lub ODM, stosuj go zgodnie z dokumentacją, ponieważ ORM injection nadal jest możliwe.
- Odrzucanie nieprawidłowych danych bez ujawniania, jak działa walidacja.
3. Znajdź kosztowne zapytanie
API jest podatne, jeśli brakuje któregokolwiek limitu zasobów albo jest on źle ustawiony. Wpis OWASP o nieograniczonym zużyciu zasobów wymienia timeouty wykonania, maksymalną przydzielaną pamięć, deskryptory plików, procesy, maksymalny rozmiar przesyłanego pliku, liczbę rekordów na stronę, liczbę operacji w jednym żądaniu klienta oraz limity wydatków u zewnętrznych dostawców.
Aby to sprawdzić, zbuduj najgłębsze zapytanie, na jakie pozwala schemat, podążając w pętli za relacjami. Dostosuj nazwy do swojego schematu:
{ users { orders { customer { orders { customer { email } } } } } }
Dodawaj kolejne poziomy, aż serwer odmówi albo zwolni. Następnie poproś o bardzo duży rozmiar strony i wyślij zbyt duży payload.
Zalecenie z cheat sheeta to dodanie kontroli, które ograniczają lub blokują zbyt kosztowne zapytania. Połącz je z limitem rozmiaru strony, timeoutem wykonania oraz maksymalnymi rozmiarami stringów, tablic i uploadów. Wartości dobierz na podstawie tygodnia rzeczywistego ruchu z logów, tak by legalni klienci mieścili się w limitach.
4. Obejdź rate limiter przez batching
Batching to brute force z ukrytą liczbą żądań. W drugim scenariuszu we wpisie OWASP mutacja uploadu stoi za rate limiterem i kontrolą rozmiaru. Atakujący wysyła jeden POST na /graphql zawierający tablicę prawie tysiąca operacji. Limiter widzi jedno żądanie, a serwerowi kończy się pamięć. Jeśli Twoje API przyjmuje uploady, ta strona zagadnienia jest opisana w naszym artykule o projektowaniu i testowaniu bezpiecznego uploadu plików.
Spróbuj tego samego na mutacji logowania lub OTP:
[
{"query": "mutation {login(user: \"alice\", pass: \"guess1\") {token}}"},
{"query": "mutation {login(user: \"alice\", pass: \"guess2\") {token}}"}
]
Sprawdź, czy Twój limit liczy każdą operację w tablicy i czy nieudane próby nadal trafiają do logiki blokady konta. Następnie wypróbuj wszystkie inne sposoby, jakie pozwala Twój serwer, na upakowanie kilku operacji w jednym żądaniu.
Ogranicz liczbę operacji w jednym żądaniu klienta i ogranicz, jak często jeden klient może wykonać pojedynczą wrażliwą operację, na przykład weryfikację OTP. Ogranicz też wydatki: w pierwszym scenariuszu z tego wpisu skrypt wywołuje dziesiątki tysięcy płatnych SMS-ów przez proces odzyskiwania hasła. Ustaw limity wydatków u zewnętrznych dostawców, a tam, gdzie limitów nie da się ustawić, alerty rozliczeniowe.
5. Poproś o cudzy atrybut
Uwierzytelnianie mówi, kto wywołuje API. Nie decyduje o tym, które pola ta osoba może odczytać. Poradnik GraphQL o autoryzacji mówi, że wykonanie powinno zacząć się dopiero po tym, jak middleware uwierzytelniający potwierdzi tożsamość, a serwer nadal musi zdecydować, dla każdego żądanego pola, czy użytkownik może je zobaczyć.
Testuj na dwóch zwykłych kontach. W sesji konta A zażądaj obiektów konta B po ID, a potem dotrzyj do nich przez powiązany obiekt, który widzą oba konta. Dostosuj nazwy do swojego schematu:
# Direct lookup: should be refused for account A
{ customer(id: "CUSTOMER_ID_OF_B") { email } }
# Same data through a shared parent: must also be refused
{ product(id: "SHARED_PRODUCT_ID") { orders { customer { email } } } }
Jeśli pierwsze zapytanie zostaje odrzucone, a drugie zwraca e-mail konta B, to kontrola jest tylko na wyszukiwaniu najwyższego poziomu i nigdy nie została zastosowana w typie zagnieżdżonym. Powtórz test całkowicie bez sesji. Jeśli te same dane są udostępniane przez REST lub mobilne API, przetestuj także te punkty wejścia. To samo podejście poza GraphQL opisuje nasz artykuł o testowaniu każdego ID w aplikacji pod kątem IDOR i BOLA.
Autoryzacja wewnątrz resolvera pola jest w porządku w prototypach, ale trzeba ją powielić dla każdego punktu wejścia. Gdy kopie zaczną się rozjeżdżać, użytkownicy widzą różne dane w zależności od tego, z którego API korzystają. Przenieś tę logikę do warstwy logiki biznesowej, aby istniało jedno źródło prawdy. W przykładzie z poradnika postRepository.getBody otrzymuje użytkownika i wpis, a treść zwraca tylko autorowi. Przekazuj w pełni zainicjalizowany obiekt użytkownika, a nie nieprzezroczysty token ani klucz API.
Czego nie wykryje skaner
Narzędzia automatyczne dają szerokie pokrycie, ale OWASP Web Security Testing Guide zwraca uwagę, że powstały z myślą o aplikacjach w ogóle, a nie o Twoim własnym kodzie i regułach biznesowych. Najpoważniejsze błędy są często splecione z logiką biznesową.
Testy od 1 do 3 da się w większości zautomatyzować. Test 5 wymaga pracy ręcznej, bo ktoś musi zrozumieć, który tenant jest właścicielem którego obiektu, więc to właśnie jego najwięcej warto zlecić zewnętrznemu zespołowi. Obejmuje to nasza usługa testów penetracyjnych.
Od czego zacząć
- Wyślij zapytanie introspekcyjne i otwórz GraphiQL na stagingu. Wyłącz to, co odpowiada, a następnie powtórz test zapytaniem z błędną składnią, żeby sprawdzić komunikaty błędów.
- Wypisz każdy argument query i mutation. Upewnij się, że każdy ma konkretny typ lub enum oraz walidator oparty na allowliście.
- Wyślij głęboko zagnieżdżone zapytanie, żądanie z dużym rozmiarem strony i zbyt duży payload. Ustaw limity, po czym wyślij zapytania ponownie i potwierdź, że każde jest odrzucane.
- Wyślij batch w tablicy na mutacje logowania i OTP. Wymuś limit na żądanie oraz na powtórzenia wrażliwych operacji.
- Wykonaj test dwóch kont, łącznie z relacjami zagnieżdżonymi, na każdym typie zawierającym dane osobowe lub handlowe. Każdą kontrolę znalezioną w resolverze przenieś do warstwy logiki biznesowej.
