Dlaczego ataki na API tak często trafiają w autoryzację
Uwierzytelnianie kontra autoryzacja – gdzie faktycznie powstaje luka
Większość zespołów dobrze rozumie uwierzytelnianie (authentication): sprawdzenie, kim jest użytkownik. Logowanie, hasło, MFA, token – to zwykle działa poprawnie. Prawdziwy problem leży w autoryzacji (authorization): czy ten konkretny użytkownik może wykonać daną operację na danym zasobie w tym kontekście.
W praktyce ogromna część ataków na API nie polega na łamaniu haseł, ale na wykorzystywaniu błędów logiki: API poprawnie rozpoznaje użytkownika, ale dopuszcza go do cudzych danych albo do operacji, których nie powinien wykonywać. Dla napastnika to złoty scenariusz – nie musi omijać logowania, wystarczy kreatywnie użyć istniejących endpointów.
Błędy autoryzacji są zdradliwe, bo nie widać ich z poziomu „szczęśliwej ścieżki”. Testy QA zazwyczaj sprawdzają, czy użytkownik może coś zrobić, rzadko sprawdzają, czy nie może tego, czego robić nie powinien. Ten brak testów negatywnych na poziomie API jest jednym z głównych powodów, dla których BOLA/IDOR tak łatwo przechodzą do produkcji.
API jako kręgosłup aplikacji i ogromna powierzchnia ataku
API stało się centralnym punktem każdej poważniejszej aplikacji: fronty SPA, aplikacje mobilne, integracje partnerów, automatyzacje wewnętrzne – wszystko opiera się na wywołaniach endpointów. To oznacza ogromną powierzchnię ataku: dziesiątki czy setki metod HTTP, setki kombinacji nagłówków, parametrów, ciał zapytań.
Wiele z tych endpointów powstaje szybko, „pod funkcję biznesową”, bez pełnego modelu uprawnień. Do tego dochodzą stare, zapomniane endpointy, wersje beta, „tymczasowe” debugowe zasoby, które nigdy nie zostały usunięte. Napastnik ma czas i narzędzia, by takie perły znaleźć. Wystarczy kilka wieczorów z Burp Suite i automatycznym fuzzingiem, by odkryć nieprzewidziane kombinacje żądań.
Jeśli projekt nie ma klarownego, centralnie opisanego modelu autoryzacji, wygra ten, kto najdłużej i najbardziej konsekwentnie będzie testował granice. Niestety zwykle nie jest to zespół developmentu, tylko ktoś po drugiej stronie internetu.
Fałszywe założenie: „jest zalogowany, więc może”
Jedno z najbardziej szkodliwych niepisanych założeń w wielu projektach brzmi: „skoro użytkownik jest zalogowany, to już wystarczy”. To prowadzi do wzorców typu:
- „Pobierz dane użytkownika z tokenu, a potem dowolnie używaj przekazanego w URL ID”.
- „Jeśli użytkownik ma rolę USER, pokaż mu wszystkie jego dane – nie filtruj dodatkowo po właścicielu”.
- „Endpoint admina jest schowany w panelu, więc zwykły użytkownik go nie znajdzie”.
Problem w tym, że UI nie jest barierą bezpieczeństwa. API można wywołać bez użycia interfejsu, wprost z Postmana, curl czy własnego skryptu. Jeśli serwer nie sprawdza, czy użytkownik jest właścicielem zasobu lub ma konkretny scope, atakujący szybko to zauważy. Rola „zalogowany użytkownik” to tylko punkt startowy, nie wystarczająca podstawa do zezwolenia na operację.
Krótki realistyczny incydent: od tokenu gościa do cudzych danych
Wyobraźmy sobie prostą aplikację do wystawiania faktur. Użytkownik może:
- pobrać listę swoich faktur:
GET /api/invoices, - pobrać fakturę po ID:
GET /api/invoices/{invoiceId}.
Front poprawnie filtruje listę. Jednak endpoint GET /api/invoices/{invoiceId} sprawdza tylko, czy użytkownik jest zalogowany. Nie weryfikuje, czy dana faktura należy do niego. Atakujący loguje się na darmowe konto, przechwytuje jedno wywołanie, podmienia identyfikator faktury w URL, iteruje po ID i zaczytuje dane innych firm. Mechanizm logowania zadziałał bezbłędnie, zawiodła autoryzacja do zasobu.
Tak wygląda większość głośnych wycieków z API: żadnego „łamania haseł”, tylko kreatywne wykorzystanie brakujących lub źle zaimplementowanych kontroli dostępu.
Podstawy bezpieczeństwa API: request, tożsamość, kontekst
Trzy kluczowe pytania przy każdym żądaniu
Każde wywołanie API powinno przejść przez ten sam mentalny filtr:
- Kto wykonuje żądanie? (konkretna tożsamość – użytkownik, serwis, integracja partnera)
- Do czego chce uzyskać dostęp? (konkretny zasób – obiekt, kolekcja, konfiguracja)
- W jakim kontekście wykonuje operację? (tenant, organizacja, rola, kanał, źródło)
Same dane z tokenu (np. e-mail, ID usera) nie wystarczą. Serwer musi powiązać je z konkretnym zasobem i operacją, a następnie podjąć jednoznaczną decyzję: pozwól / zabroń. Brak tego powiązania tworzy przestrzeń na BOLA, Broken Function Level Authorization i inne błędy autoryzacji.
Dobrą praktyką jest ujednolicenie tego procesu: centralny moduł lub biblioteka, która na wejściu dostaje podmiot–zasób–operacja–kontekst i zwraca decyzję (ALLOW/DENY). Ad hoc sprawdzanie ról w różnych miejscach kodu prowadzi do chaosu i niespójności.
Warstwy zabezpieczeń: od klienta do bazy danych
Bezpieczeństwo API to kilka powiązanych warstw, które muszą ze sobą współgrać:
- Klient (front, aplikacja mobilna, integracja) – nie może przechowywać sekretów, które „odblokowują” pełne uprawnienia; to tylko kanał, nie element zaufany.
- API gateway – pierwsza linia obrony: terminacja SSL, autoryzacja wstępna, rate limiting, weryfikacja tokenu, czasem filtrowanie pól odpowiedzi.
- Serwisy backend – właściwe reguły autoryzacji biznesowej: kto może co zrobić na jakim zasobie; logika powinna być tu, a nie w UI.
- Baza danych – dodatkowa warstwa: row-level security, ograniczenia tenantów, widoki; nie zastępuje autoryzacji w serwisie, ale ją wzmacnia.
Typowy błąd: całą odpowiedzialność przesunąć do jednej warstwy (np. tylko do gatewaya). Gateway zna token, ale nie zna pełnego kontekstu biznesowego danego zasobu. Z kolei serwis bez walidacji tokenu przyjmuje każde połączenie z sieci „wewnętrznej”. Spójny model wymaga, by każda warstwa robiła swoje: gateway – techniczną walidację, serwis – biznesową autoryzację, baza – dodatkową ochronę przed błędami serwisu.
Model podmiot–zasób–operacja w praktyce
Dobrym sposobem myślenia o autoryzacji jest prosty model:
- podmiot (subject): użytkownik, rola, serwis techniczny, klient OAuth,
- zasób (resource): pojedynczy obiekt (np. faktura), kolekcja, konfiguracja systemowa,
- operacja (action): odczyt, zapis, usuwanie, eksport, zmiana roli, restart joba,
- kontekst: organizacja/tenant, kanał (API publiczne vs internal), IP, czas.
Każdy endpoint powinien być dający się opisać w tym modelu. Na przykład:
GET /api/invoices/{id}→ podmiot: zalogowany użytkownik; zasób: faktura; operacja: odczyt; kontekst: tenant użytkownika.POST /api/admin/users/{id}/roles→ podmiot: użytkownik z rolą ADMIN; zasób: role użytkownika; operacja: modyfikacja; kontekst: ten sam tenant.
Brak takiego opisu często oznacza, że endpoint powstał „przy okazji”, bez namysłu nad uprawnieniami. To pierwsza czerwona flaga z perspektywy testera bezpieczeństwa.
OAuth2, OIDC, JWT – gdzie pomagają, a czego nie załatwiają
Standardy typu OAuth2, OpenID Connect czy JWT często są traktowane jako „pełne rozwiązanie bezpieczeństwa”. W rzeczywistości rozwiązują one głównie problem uwierzytelnienia i delegowania uprawnień na wysokim poziomie (scopes). Nie projektują za nas logiki „czy użytkownik może pobrać ten konkretny rekord z bazy”.
OAuth2 daje mechanizm uzyskania tokenu z określonym zakresem. OIDC rozszerza go o tożsamość użytkownika. JWT to format przenoszenia tych informacji. Jednak decyzja: czy użytkownik z tokenem o nazwie scope=INVOICES_READ może pobrać fakturę o ID=123 z tenant=XYZ leży po stronie serwisu API. Brak tego dodatkowego poziomu kontroli prowadzi do BOLA i Broken Function Level Authorization.
Standardy są ważnym budulcem, ale autoryzacja zasobów i operacji to warstwa logiki aplikacyjnej, którą trzeba zaprojektować, zaimplementować i przetestować niezależnie.

Najczęstsze klasy ataków na autoryzację API (mapa ryzyka)
BOLA / IDOR – przejmowanie cudzych zasobów
BOLA (Broken Object Level Authorization), znane też jako IDOR (Insecure Direct Object References), to jeden z najczęściej wykorzystywanych błędów w API. Mechanizm jest prosty:
- endpoint identyfikuje zasób po ID w ścieżce, parametrze lub ciele,
- serwer nie sprawdza, czy użytkownik ma prawo do tego konkretnego zasobu,
- atakujący zmienia ID i odczytuje/edytuje cudze dane.
Przykład schematyczny: GET /api/users/123/profile zwraca profil użytkownika 123 dla dowolnego zalogowanego użytkownika. Brakuje sprawdzenia: userFromToken.id == pathParam.id lub innego powiązania właściciela z zasobem. W efekcie /api/users/124/profile również działa.
Ataki BOLA są bardzo łatwe do automatyzacji. Narzędzia typu Burp Intruder, ffuf czy własne skrypty mogą masowo iterować po identyfikatorach i zbierać dane. Wiele głośnych wycieków danych z ostatnich lat bazowało właśnie na tym wzorcu.
Broken Function Level Authorization – za szerokie uprawnienia do operacji
Druga popularna klasa to błędy na poziomie operacji. API poprawnie broni dostępu do obiektu (np. filtruje listę faktur do właściciela), ale nie weryfikuje, kto może wykonać konkretne akcje na tych obiektach. Skutkiem są sytuacje, gdzie zwykły użytkownik może:
- włączyć endpoint eksportu wszystkich danych (
/admin/export), - zmienić role innym użytkownikom (
/users/{id}/roles), - odpalić masowe operacje wsadowe (
/jobs/recalculate-all).
Tu nie wystarczy sprawdzać „czy jest zalogowany” ani nawet „czy jest właścicielem zasobu”. Konieczne jest precyzyjne powiązanie roli lub atrybutów podmiotu z typem operacji, nie tylko z obiektem.
Broken User Authentication – tokeny i sesje jako słaby punkt
Choć główny temat to autoryzacja, błędne uwierzytelnianie często toruje drogę do jej obejścia. Klasyczne problemy:
- tokeny o bardzo długim czasie życia, które po wycieku działają miesiącami,
- brak rotacji refresh tokenów i sprawdzania ich „spalenia”,
- słaba walidacja podpisu JWT (np. akceptacja algorytmu
none), - przekazywanie tokenu w URL, gdzie trafia do logów i historii przeglądarki.
Atakujący, który wejdzie w posiadanie ważnego tokenu, korzysta z API tak jak uprawniony użytkownik. Wtedy wszystkie błędy autoryzacji stają się natychmiast krytyczne, bo nie ma już bariery logowania. Z tego powodu model tokenów i sesji jest integralną częścią bezpieczeństwa API.
Excessive Data Exposure i Mass Assignment
Często zakłada się, że skoro użytkownik ma dostęp do danego endpointu, to może zobaczyć całą odpowiedź lub wysłać dowolne pole w ciele żądania. Powstają dwa problemy:
- Excessive Data Exposure – API zwraca zbyt wiele danych (np. pola wewnętrzne, flagi admina, tokeny integracji), a UI tylko „chowa” je przed użytkownikiem.
- Mass Assignment – API bez selekcji mapuje wszystkie pola z wejścia na model w bazie; atakujący dodaje w żądaniu ukryte pola, których UI nie wysyła (np.
isAdmin=true).
Takie błędy często występują w połączeniu z BOLA lub Broken Function Level Authorization, zwiększając szkody. Nawet poprawnie ograniczony zasób może wyciekać zbyt wiele informacji, które pomagają w dalszej eskalacji.
Rate limiting, brute force i automatyzacja ataków
Autoryzacja to nie tylko pytanie „kto co może”, ale także „jak często może próbować”. Brak limitów żądań na endpointach logowania, resetu hasła czy weryfikacji kodów MFA otwiera furtkę dla ataków brute force i credential stuffing. Podobnie, brak throttlingu na krytycznych endpointach (np. BOLA-testowalnych) umożliwia masowe skanowanie ID w krótkim czasie.
Defense-in-depth wymaga połączenia:
Łączenie kontroli autoryzacji z mechanizmami anty-automatyzacyjnymi
Same reguły „kto co może” nie wystarczą, jeśli atakujący może wykonywać tysiące prób na minutę. Przy krytycznych akcjach techniczne zabezpieczenia powinny być spięte z autoryzacją.
- Rate limiting per podmiot – limity nie tylko per IP, ale także per subject z tokenu (user_id, client_id). Zabezpiecza przed nadużyciami z przejętych kont.
- Limity per zasób – np. max 5 zmian hasła na konto dziennie, max 3 eksporty wszystkich danych na tenant w godzinę.
- Progressive delays – im więcej błędnych prób, tym większa zwłoka odpowiedzi (bez jasnego komunikatu, że to limit).
- Hard cap – po określonej liczbie podejrzanych prób blokada konta/klienta i wymuszona ścieżka odzyskiwania.
To szczególnie ważne przy testowaniu autoryzacji. Zespół bezpieczeństwa musi umieć przeprowadzić masowe testy BOLA/IDOR bez blokowania samego siebie, ale jednocześnie system nie może być zupełnie otwarty na zewnętrzne skanowanie.
BOLA / Broken Object Level Authorization – najgroźniejszy błąd API
Jak rozpoznać, że API jest podatne na BOLA
W praktyce pierwsza rzecz, którą warto zrobić przy audycie API, to zmapowanie, gdzie pojawiają się identyfikatory zasobów:
- w ścieżce –
/users/{id},/orders/{orderId}, - w query –
/invoices?invoiceId=123, - w ciele żądania –
{"userId": 123, "role": "ADMIN"}, - w nagłówkach niestandardowych – np.
X-Company-Id.
Każde takie ID to potencjalny punkt BOLA. Jeśli logika serwisu nie wiąże go jednoznacznie z podmiotem z tokenu (lub z tenantem), drzwi są uchylone.
Typowe anty-wzorce prowadzące do BOLA
Błędy BOLA rzadko są „efektem złej woli”. Zwykle wynikają z wygody i pośpiechu:
- „Zaufane ID” – przyjęcie, że skoro ID przyszło z frontendu, to UI już je „filtrował”. Backend ślepo wierzy parametrom.
- Filtrowanie po liście, nie po pojedynczym rekordzie –
GET /invoicesma filtr po owner_id, aleGET /invoices/{id}już nie. - Brak powiązania z tenantem – ID jest globalne w bazie, a filtr jest tylko po user_id; atakujący spoza tenanta zgaduje ID i trafia w zasób innej organizacji.
- Warstwa cache bez autoryzacji – serwis cache’ujący trzyma odpowiedzi po
invoiceId, ale nie bierze pod uwagęuserId.
Minimalny wzorzec obrony przed BOLA
Najprostsza i jednocześnie najskuteczniejsza obrona to konsekwentny wzorzec w kodzie. Dobrze sprawdza się reguła „najpierw zasób, potem sprawdzenie właściciela”:
- Pobierz zasób po ID w ramach tenanta lub innego kontekstu (np. firmowego).
- Sprawdź, czy podmiot z tokenu jest właścicielem lub ma inne uprawnienie do tego zasobu.
- Dopiero wtedy wykonaj operację (odczyt, zapis, usunięcie).
Jeśli model danych na to pozwala, dobrym trikiem jest w ogóle nie pozwalać na zapytania po „gołym” ID. Zamiast:
SELECT * FROM invoices WHERE id = :id;
lepiej wymusić:
SELECT * FROM invoices
WHERE id = :id AND tenant_id = :tenantId;
Przy multi-tenancy daje to podwójne zabezpieczenie: logika aplikacyjna oraz twarde ograniczenie na poziomie SQL.
Praktyczne testy BOLA – checklista dla pentestera
Podczas testów bezpieczeństwa prosta lista kroków często wystarcza, by odkryć poważne luki:
- Wyłap wszystkie endpointy z ID w ścieżce, query lub body.
- Utwórz dwa konta w różnych tenantach (lub z różnymi rolami) i wygeneruj dla nich tokeny.
- Na koncie A wykonaj legalne żądanie, zapisz całą odpowiedź i ID zasobu.
- Powtórz żądanie z tokenem konta B, podkładając ID zasobu z konta A.
- Sprawdź, czy:
- otrzymujesz dane, do których B nie powinno mieć dostępu,
- masz możliwość edycji lub usunięcia zasobu A z konta B,
- występują różnice w czasie odpowiedzi (może to zdradzać częściowe sprawdzenia).
W narzędziach typu Burp warto ustawić Intrudera na sekwencyjną podmianę ID, ale przy systemach produkcyjnych lepiej robić to ostrożnie – z ograniczonym zakresem i w uzgodnionym oknie.

Broken Function Level Authorization – operacje poza zakresem roli
Kiedy obiekt jest chroniony, ale funkcja już nie
Wiele serwisów ma przyzwoitą ochronę samego zasobu. Użytkownik widzi tylko swoje dane. Problem zaczyna się przy „specjalnych” akcjach:
- akcje masowe:
/users/export,/orders/recalculate, - operacje administracyjne: zmiana ról, blokowanie kont, resetowanie haseł innym,
- funkcje serwisowe: restart batcha, czyszczenie cache, regeneracja indeksów.
Często są ukryte tylko w panelu admina i zespół zakłada, że „normalny” użytkownik nigdy ich nie zobaczy. W testach API takie endpointy widać od razu – szukanie ścieżek zawierających /admin, /internal, /maintenance to rutyna.
Mapowanie operacji na role – bez „if admin” w losowych miejscach
Żeby uniknąć chaosu, operacje trzeba najpierw nazwać i opisać:
- Lista funkcji – np.
USER_MANAGE,USER_ROLE_ASSIGN,INVOICE_EXPORT_ALL,SYSTEM_JOB_RESTART. - Mapowanie na role – osobna tabela/konfiguracja: jaka rola/system może wykonać daną funkcję.
- Spójny mechanizm sprawdzania – np.
requirePermission("INVOICE_EXPORT_ALL")na wejściu do każdego endpointu.
Rozsypywanie po kodzie if user.isAdmin() sprawia, że po roku nikt nie wie, co tak naprawdę umie ta rola. Każda nowa funkcja „dla admina” to dodatkowy warunek i kolejne miejsce podatne na pomyłkę.
Testowanie Broken Function Level Authorization w praktyce
Dobrze działa prosty eksperyment z „obniżaniem uprawnień”:
- Zaloguj się jako użytkownik z najwyższymi uprawnieniami i nagraj wszystkie wywołania API, korzystając z panelu admina.
- Zaloguj się jako zwykły użytkownik (lub inna rola bez uprawnień administracyjnych).
- Odegraj zapisane requesty, zmieniając tylko token na „słabszy”.
- Obserwuj odpowiedzi:
- HTTP 403/401 – spodziewany wynik,
- HTTP 200/204 – najczęściej poważny błąd,
- HTTP 404 – bywa formą „security by obscurity”; endpoint nie powinien być dostępny logicznie, a nie tylko „niewidoczny”.
Czasem testy wychwytują sytuacje, gdy UI blokuje dostęp do funkcji, ale backend ufa pochodzeniu żądania, np. przyjmuje hidden field z formularza tylko dlatego, że „pochodzi z panelu admina”. W API nie ma takiego rozróżnienia – każde żądanie jest równorzędne.
Tokeny i sesje w API: JWT, cookies, nagłówki, typowe pułapki
Gdzie trzymać token i jak go przekazywać
API to nie tylko backend do SPA. W grze są przeglądarki, aplikacje mobilne, integracje serwer–serwer. Każdy kanał ma własne ograniczenia.
- Przeglądarka:
- bezpieczniejsze są
HttpOnly Secure SameSitecookies do przechowywania access/refresh tokenów, - trzymanie JWT w
localStoragenaraża na XSS, a XSS w praktyce zdarza się regularnie.
- bezpieczniejsze są
- Mobile / desktop:
- dedykowane storage OS (Keychain, Keystore) zamiast plików tekstowych,
- unikanie logowania tokenów w crash reportach i analityce.
- Integracje serwer–serwer:
- nagłówek
Authorization: Bearer <token>jako standard, - automatyczne odświeżanie i rotacja kluczy/sekretów.
- nagłówek
Jedna prosta zasada: token nie może się pojawiać w URL (query, fragment). Trafia wtedy do logów proxy, historii przeglądarki i analityki.
JWT – elastyczność kontra bezpieczeństwo
JWT kusi możliwością przeniesienia wielu informacji w podpisanym tokenie. W praktyce to jednocześnie siła i pułapka.
- Czas życia – im dłużej ważny jest access token, tym większe ryzyko nadużycia po wycieku. Rozsądny wzorzec: krótki access token (minuty) + dłuższy refresh token (godziny/dni).
- Walidacja algorytmu – biblioteka powinna wymuszać konkretny algorytm (np.
RS256), a nie ufać nagłówkowi z tokenu. - Podpis vs szyfrowanie – podpis (JWS) nie ukrywa danych, tylko gwarantuje ich integralność. Wrażliwe dane w payloadzie mogą być widoczne dla klienta.
- Rozmiar tokenu – upychanie pełnej listy uprawnień w tokenie powoduje rozrost i problemy z nagłówkami, a aktualizacja uprawnień wymaga odnowienia wszystkich tokenów.
Najczęstsze błędy przy pracy z JWT
Kilka wzorców, które pojawiają się zaskakująco często:
- Akceptacja tokenów bez sprawdzania
expinbf– „wiecznie żywe” sesje. - Brak walidacji
audienceiissuer– serwis przyjmuje tokeny wystawione dla innych systemów. - Złe obchodzenie się z kluczami – prywatny klucz w repozytorium, jeden klucz dla wszystkich środowisk.
- Zmiana listy uprawnień bez mechanizmu revokacji – przyznane raz scope’y działają do końca
exp, nawet jeśli konto już nie powinno ich mieć.
Sesje vs stateless JWT – kompromisy
W API często pada hasło „stateless”, ale rzeczywistość jest bardziej złożona:
- Czysty JWT (stateless):
- brak centralnej listy sesji, łatwe skalowanie,
- trudna revokacja – dopóki token nie wygaśnie, jest ważny, chyba że wprowadzimy listy blokujące.
- Sesje serwerowe:
- pełna kontrola nad życiem sesji (logout natychmiast działa),
- wymaga współdzielonego storage (redis, baza) w klastrze.
Praktyczny kompromis: krótko żyjący JWT jako access token + stan serwerowy lub lista revokacji dla refresh tokenów i wyjątkowych przypadków (np. blokada konta). Autoryzacja w API powinna zakładać, że token może zostać unieważniony jeszcze przed exp.
Testy bezpieczeństwa tokenów i sesji
Przy audycie warto przejść kilka prostych scenariuszy:
- Przejąć token (np. przez proxy) i sprawdzić:
- czy działa po zmianie hasła,
- czy działa po ręcznym wylogowaniu,
- czy działa z innych adresów IP/lokalizacji.
- Zmanipulować datami w JWT (jeśli klucz jest znany w środowisku testowym) i zobaczyć, czy backend je respektuje.
- Wymusić użycie tokenu wystawionego dla innego klienta (inna
aud) i API, aby sprawdzić walidację odbiorcy.
W praktyce często wychodzi, że token działa długo po tym, jak użytkownik formalnie nie ma już dostępu do systemu, bo proces offboardingu obejmuje tylko bazę użytkowników, a nie sesje/API.

Granice między serwisami: autoryzacja w mikroserwisach i API gateway
Zaufana sieć wewnętrzna – klasyczny mit
W architekturach mikroserwisowych często pojawia się założenie: „jak request przeszedł przez gateway, to w środku już wszystko jest zaufane”. To prosta droga do krytycznych błędów autoryzacji między serwisami.
Delegowanie zaufania zamiast „przekazywania roli”
Bezpieczniejszy model to jasne rozdzielenie odpowiedzialności:
- Gateway – weryfikuje tożsamość, podstawowe atrybuty (ID użytkownika, klienta, tenant), wykonuje globalne polityki (rate limiting, geo-block, blokady kont).
- Serwisy biznesowe – egzekwują reguły autoryzacji dla swoich danych i operacji. Nie ufają ślepo temu, że request „przyszedł z gatewaya”.
Przekazywane do serwisów dane o użytkowniku powinny być minimalne i podpisane (np. JWT z nadanym przez IdP/gateway zasięgiem, a nie surowy JSON z rolami z nagłówka). Serwis na tej podstawie może dociągnąć dodatkowy kontekst z własnej bazy lub cache’u.
Jak bezpiecznie przekazywać tożsamość między serwisami
Typowy problem: serwis A woła serwis B „w imieniu” użytkownika. Da się to zrobić bez rozlewania tokenów końcowego użytkownika po całej infrastrukturze.
- Token serwisowy – serwis A ma własny techniczny credential (m2m), którym uwierzytelnia się do serwisu B.
- Delegowany kontekst użytkownika – w nagłówku lub claimach przekazywany jest identyfikator użytkownika i ewentualne scope’y, ale:
- pochodzą one z zaufanego źródła (IdP/gateway),
- są podpisane/jako część tokenu,
- serwis B waliduje zarówno „kto woła” (client/serwis), jak i „w czyim imieniu” (user).
Niebezpieczny antywzorzec: serwis B przyjmuje nagłówek X-User-Id bez podpisu i uznaje go za prawdę, bo „i tak może do niego zadzwonić tylko A”. W praktyce w wystarczająco złożonych środowiskach znajdzie się inny komponent mogący odtworzyć ten nagłówek.
Typowe błędy autoryzacji w mikroserwisach
W rozproszonych systemach powtarzają się podobne potknięcia:
- Autoryzacja tylko na wejściu do systemu – gateway sprawdza scope’y, role, a wewnątrz serwisy wykonują wszystko, co zostanie im zlecone.
- Brak separacji ról technicznych i użytkowników – ten sam token docelowego użytkownika krąży między serwisami, zamiast lokalnych uprawnień dla serwis–serwis.
- Uproszczone role „internal service” – token z rolą
SERVICE_INTERNALma prawo do wszystkiego w każdym mikroserwisie. - Mieszanie modeli tenantów – jeden serwis rozumie
tenantId, innyorganizationId; brak spójnego sprawdzania przynależności obiektu do najemcy.
Testowanie autoryzacji międzyserwisowej
Kilka prostych eksperymentów jest w stanie obnażyć luki na granicach:
- Spróbuj wywołań „z pominięciem gatewaya” (np. bezpośrednie IP serwisu w Kubernetes) z tokenem użytkownika – sprawdź, czy serwis coś jeszcze weryfikuje.
- Jeśli masz dostęp do komunikacji międzyserwisowej, zmodyfikuj nagłówki z kontekstem użytkownika (ID, role, tenant) i obserwuj, czy serwis B wykrywa niespójność.
- Zasymuluj błędnie skonfigurowany serwis: wywołaj endpoint, który według dokumentacji ma wymagać roli admina, przy tokenie z minimalnymi uprawnieniami – często okaże się, że serwis ufa tylko temu, że request „przyszedł z wewnątrz”.
API gateway jako centralny enforcement point – pułapki
Gateway kusi możliwością trzymania wszystkich reguł „w jednym miejscu”. To działa tylko do granicy prostych scope’ów.
Przy bardziej złożonych regułach (np. „manager może zmieniać dane tylko swoich podwładnych” albo „użytkownik widzi wyłącznie kontrakty w swoim oddziale”) gateway nie ma pełnego kontekstu danych. Nie zna struktury organizacyjnej ani relacji między obiektami. Takie warunki i tak trzeba sprawdzić w serwisie domenowym.
Rozsądny kompromis:
- gateway sprawdza:
- ważność i poprawność tokenu (algorytm, aud, iss, exp),
- podstawowe scope’y (np.
orders.read,orders.write), - wysokopoziomowe polityki (np. geo, device, rate limit),
- serwisy sprawdzają:
- czy użytkownik ma prawo do konkretnego obiektu (BOLA),
- czy może wykonać konkretną operację na tym obiekcie (function-level),
- czy żądanie zgodne jest z lokalnymi regułami biznesowymi.
Mikro-checklista dla projektowania autoryzacji w mikroserwisach
Przy projektowaniu nowej architektury rozproszonych serwisów przydaje się krótka lista kontrolna:
- Każdy serwis ma jasno zdefiniowany model uprawnień (jakie operacje, jakie role/scope’y lokalne).
- Tokeny użytkowników nie są bezrefleksyjnie „przekazywane dalej” – między serwisami krąży podpisany kontekst i osobne credentiale m2m.
- Serwisy walidują audience – token wystawiony dla serwisu X nie działa do serwisu Y.
- Wejście „bocznymi drzwiami” (bezpośrednie IP, serwis mesh, debug endpoints) ma taki sam model autoryzacji jak ruch przez gateway.
- Na poziomie organizacyjnym jest ustalone, kto jest właścicielem reguł autoryzacji dla poszczególnych domen (np. billing, CRM), aby uniknąć sprzecznych konfiguracji.
Projektowanie autoryzacji w API: praktyczny model krok po kroku
Krok 1: Zrozum aktorów i wektory ataku
Zanim powstanie pierwsza linijka kodu autoryzacji, trzeba nazwać, kto będzie z API korzystał – i kto może próbować je nadużyć.
- Aktorzy legalni:
- użytkownicy końcowi (przez web/mobile),
- partnerzy B2B (integracje),
- wewnętrzne systemy batchowe/jobowe.
- Aktorzy atakujący:
- użytkownik z ważnym kontem, próbujący „wyjść poza swoją rolę”,
- partner integracyjny testujący granice scope’ów,
- anonimowy skaner API szukający źle zabezpieczonych endpointów.
Dla każdego aktora pomocne jest krótkie zdanie: „Czego nie powinien móc zrobić?”. To często od razu odsłania potrzebne ograniczenia.
Krok 2: Wypisz operacje, a nie tylko endpointy
Większość projektów zaczyna od „listy endpointów”. Lepsze podejście to lista operacji biznesowych:
USER_VIEW_SELF,USER_VIEW_ALL,USER_MANAGE_ROLES,INVOICE_VIEW_OWN,INVOICE_VIEW_TENANT,INVOICE_CANCEL,REPORT_EXPORT_OWN,REPORT_EXPORT_ALL, itd.
Każdy endpoint API powinien być tylko implementacją jednej lub kilku takich operacji. To ułatwia zarówno implementację, jak i audyt. Zamiast zgadywać z URL-a, co coś robi, patrzysz na to, jakich funkcji wymaga.
Krok 3: Zbuduj model ról i scope’ów
Ślepe rzucanie „ROLE_ADMIN” i „ROLE_USER” kończy się szybko bałaganem. Potrzebny jest świadomy model:
- Role biznesowe – np.
ROLE_AGENT,ROLE_MANAGER,ROLE_ACCOUNTANT. - Scope’y techniczne – np.
orders.read,orders.write,users.managedla OAuth2.
Role biznesowe mapujesz na zestawy funkcji. Scope’y techniczne kontrolują, do których grup API w ogóle można się dostać; szczegółowe decyzje podejmują serwisy na podstawie ról/funkcji.
Krok 4: Zdefiniuj model własności danych
Bez jasnego modelu własności nie zrobisz poprawnej autoryzacji obiektów (BOLA). Kilka pytań, na które trzeba odpowiedzieć:
- Czy obiekt ma jednego właściciela (np.
userId), czy wielu (np. zespół, oddział, tenant)? - Czy własność może się zmieniać w czasie (przeniesienie klienta między opiekunami)?
- Jak prezentować „widok managera” – czy widzi dane podwładnych, zespołu, całego działu?
Na tej podstawie powstaje konkretny zestaw reguł, np.:
- użytkownik widzi tylko obiekty, gdzie
ownerId == userId, - manager widzi obiekty, gdzie
ownerIdnależy do listy jego podwładnych, - rola centralna widzi wszystkie obiekty w ramach
tenantId.
Krok 5: Zaprojektuj centralną bibliotekę autoryzacji
Rozrzucanie warunków typu if (user.isAdmin()) po kodzie gwarantuje powstawanie luk. Potrzebny jest wspólny mechanizm:
- Funkcja typu
authorize(userContext, operation, resourceContext), - lub adnotacje / middleware per endpoint (np.
@RequiresPermission("INVOICE_CANCEL")).
Implementacja może korzystać z konfiguracji (np. YAML/DB) z mapą: operacja → wymagane role/scope’y / dodatkowe warunki. Dzięki temu zmiana reguły nie wymaga refaktoringu całego API.
Krok 6: Ustal, co jest sprawdzane globalnie, a co lokalnie
Część reguł ma sens wszędzie (np. blokada konta, wygaśnięcie hasła, ogólny limit uprawnień). Inne są stricte lokalne dla domeny.
- Globalne:
- konto zablokowane / wymuszenie zmiany hasła,
- użytkownik wylogowany ze wszystkich sesji,
- ogólne uprawnienie do korzystania z API (np. „czy konto jest aktywne w tym systemie”).
- Lokalne:
- czy użytkownik może anulować konkretne zamówienie,
- czy może zobaczyć dane finansowe danego oddziału,
- czy ma prawo edytować ustawienia wybranego klienta.
Globalne reguły dobrze jest egzekwować jak najbliżej miejsca uwierzytelnienia (IdP/gateway). Lokalnymi zajmują się serwisy domenowe.
Krok 7: Zaplanuj revokację i zmianę uprawnień
System autoryzacji bez realnej możliwości cofnięcia dostępu jest iluzją. W projekcie trzeba przewidzieć:
- Jak są unieważniane aktywne sesje/tokeny po:
- zmianie ról użytkownika,
- odejściu z firmy / zakończeniu współpracy,
- podejrzeniu wycieku (incident response).
- Czy istnieje centralna lista revokowanych tokenów / kluczy (dla JWT, API keys).
- Jak szybko serwisy widzą zmiany ról (cache, TTL, eventy domenowe).
Bez tego test penetracyjny bardzo często pokaże, że „stary” token z szerszymi uprawnieniami działa jeszcze długo po tym, jak w bazie użytkownik ma już odebrane prawa.
Krok 8: Zadbaj o obserwowalność naruszeń autoryzacji
Same reguły to za mało. Potrzebne są sensowne logi i metryki. W praktyce przydaje się:
- logowanie odmów autoryzacji z minimalnym, lecz użytecznym kontekstem (user/tenant/operation/resourceId, ale bez wrażliwych danych),
- metryki typu:
- liczba HTTP 403 per endpoint/rola,
- nietypowe skoki 403 z jednego IP/klienta – często sygnał prób enumeracji,
- użycie rzadkich operacji administracyjnych.
Dobrze zrobiona autoryzacja generuje „szum kontrolowany” – wiele bezpiecznie zablokowanych prób, które można analizować i na tej podstawie ulepszać polityki.
Krok 9: Scenariusze testów autoryzacji do powtarzania
Testy autoryzacji nie mogą się kończyć na jednym pentestcie. Warto mieć podstawowy zestaw scenariuszy do powtarzania przy każdej większej zmianie:
- Testy pozytywne – użytkownik z daną rolą/scope’em:
- ma dostęp do wszystkich funkcji, które powinien mieć,
- nie napotyka „losowych” 403 przy poprawnych przepływach.
- Testy negatywne – użytkownik:
- nie może wykonać operacji przypisanej wyłącznie do wyższej roli,
- nie może odczytać/zapisać obiektu innego użytkownika/tenanta,
Najczęściej zadawane pytania (FAQ)
Na czym polega różnica między uwierzytelnianiem a autoryzacją w API?
Uwierzytelnianie (authentication) odpowiada na pytanie „kim jesteś?” – chodzi o sprawdzenie tożsamości użytkownika lub serwisu, np. przez login/hasło, MFA, token OAuth2. Jeśli ten etap działa, system wie, czyje żądanie obsługuje.
Autoryzacja (authorization) odpowiada na pytanie „co wolno ci zrobić na jakim zasobie w danym kontekście?”. To tu decydujesz, czy zalogowany użytkownik może pobrać konkretną fakturę, zmienić rolę innego użytkownika czy wywołać endpoint administracyjny.
Typowe incydenty w API wynikają z tego, że zespół skupia się na mocnym uwierzytelnianiu, a autoryzację sprowadza do prostego sprawdzenia roli lub samego faktu „jest zalogowany”. Atakujący nie musi łamać loginu i hasła – wystarczy, że wykorzysta brak kontroli nad dostępem do zasobów.
Czym jest BOLA/IDOR i dlaczego tak często występuje w API?
BOLA (Broken Object Level Authorization), znane też jako IDOR (Insecure Direct Object Reference), to błąd, w którym API nie sprawdza, czy użytkownik jest właścicielem obiektu, do którego się odwołuje. Przykład: endpoint GET /api/invoices/{invoiceId} działa dla dowolnego ID, byle użytkownik był zalogowany.
Atakujący rejestruje zwykłe konto, przechwytuje jedno wywołanie, podmienia identyfikator w ścieżce i iteruje po ID. Jeśli backend nie wiąże obiektu z właścicielem (tenantem, organizacją), zaczyna zwracać cudze dane. UI może filtrować listę poprawnie – incydent i tak wydarzy się bez udziału interfejsu.
BOLA jest powszechne, bo testy QA najczęściej sprawdzają scenariusze „szczęśliwej ścieżki”: czy użytkownik może coś zrobić. Rzadko sprawdzają, czy nie może wykonać operacji na cudzych zasobach. Brak systematycznych testów negatywnych na poziomie API to paliwo dla takich ataków.
Dlaczego samo zalogowanie użytkownika nie wystarcza do zabezpieczenia API?
Fakt, że ktoś jest zalogowany, oznacza tylko, że znamy jego tożsamość. Nic nie mówi o tym, jakie ma uprawnienia do konkretnego zasobu i operacji. Założenie „jest zalogowany, więc może” zamienia się w błędy typu: brak sprawdzenia właściciela zasobu, używanie ID z URL bez walidacji czy ukrywanie endpointów admina „za frontendem”.
API można wywołać bez UI – z Postmana, curl, własnego skryptu. Jeśli backend nie weryfikuje, czy dany użytkownik jest właścicielem rekordu lub ma odpowiedni scope/rolę w danym kontekście (tenant, organizacja, kanał), napastnik to znajdzie. Interfejs użytkownika nie jest kontrolą bezpieczeństwa, tylko wygodnym klientem.
Bez twardej autoryzacji na poziomie API szybko dochodzi do scenariuszy, w których konto „gościa” wyciąga dane firm produkcyjnych, choć logowanie, sesje i MFA działają książkowo.
Jak praktycznie testować autoryzację API, żeby wykrywać BOLA i podobne błędy?
Podstawą są testy negatywne z różnych typów kont. Dla kluczowych endpointów przeprowadź prostą checklistę:
- Ten sam request z innym zalogowanym użytkownikiem – czy nadal widzi dane?
- Podmiana ID zasobu w ścieżce/parametrze – czy można uzyskać cudze rekordy?
- Użycie konta o niższych uprawnieniach (USER zamiast ADMIN) – czy backend odrzuca żądanie?
Do takich testów najczęściej używa się Burp Suite, Postmana lub prostych skryptów. Dobrym wzorcem jest systematyczne przechodzenie po modelu: podmiot–zasób–operacja–kontekst. Dla każdego endpointu zadaj pytanie: czy backend jasno weryfikuje, że ten konkretny podmiot może wykonać tę operację na tym zasobie w tym kontekście. Jeśli nie umiesz tego łatwo wskazać w kodzie lub w polityce, to kandydat na podatność.
Jak zbudować poprawny model autoryzacji w API (podmiot–zasób–operacja–kontekst)?
Praktyczne podejście to opisanie każdego endpointu w czterech elementach: kto (podmiot), do czego (zasób), co chce zrobić (operacja) i w jakich warunkach (kontekst). Przykład: GET /api/invoices/{id} – podmiot: zalogowany użytkownik; zasób: pojedyncza faktura; operacja: odczyt; kontekst: tylko w ramach własnego tenanta/organizacji.
Na tej bazie opłaca się zbudować centralny mechanizm autoryzacji: moduł lub biblioteka, która dostaje podmiot–zasób–operacja–kontekst i zwraca decyzję ALLOW/DENY. Zamiast rozrzucać ify po kontrolerach, wszystkie serwisy wołają jeden, spójny komponent, a logika reguł jest w jednym miejscu.
Brak formalnego modelu prowadzi do „ad hoc” sprawdzania ról, specjalnych wyjątków w kodzie i niespójności między endpointami. To dokładnie te miejsca, które audytor i atakujący będą atakować w pierwszej kolejności.
Czy OAuth2, OIDC i JWT rozwiązują problem autoryzacji w API?
OAuth2 i OpenID Connect rozwiązują głównie uwierzytelnianie oraz delegowanie dostępu na wysokim poziomie. Dostajesz token z określonym zakresem (scope) i informacją o tożsamości użytkownika. JWT to tylko nośnik tych informacji w postaci podpisanego tokenu.
Decyzja „czy użytkownik ze scope=INVOICES_READ może odczytać fakturę o ID=123 w tenancie XYZ” pozostaje po stronie twojego API. Standardy nie znają twojego modelu danych, tenantów, ról biznesowych ani logiki właściciela zasobu. Jeśli zatrzymasz się na samym sprawdzeniu „token poprawny, scope obecny”, droga do BOLA i Broken Function Level Authorization stoi otworem.
Token traktuj jako wejście do systemu autoryzacji, nie jako jej pełne zastępstwo. Najpierw weryfikacja tokenu (gateway), potem decyzja biznesowa na poziomie serwisu (podmiot–zasób–operacja–kontekst), a na końcu ewentualne wymuszenie izolacji w bazie (np. row-level security dla tenantów).
Jakie warstwy powinny uczestniczyć w zabezpieczeniu API i jak nie popełnić typowych błędów?
Bezpieczne API to współpraca kilku warstw. Klient (frontend, aplikacja mobilna, integracja) nie jest zaufany – nie wkładaj do niego „sekretów rootowych”. API gateway powinien przejąć SSL, weryfikację tokenu, podstawową autoryzację techniczną i limity żądań. Serwisy backendowe odpowiadają za właściwą autoryzację biznesową, czyli decyzje typu „czy ten użytkownik może zmienić tego konkretnego użytkownika / fakturę / konfigurację”. Baza danych może dodatkowo wymuszać izolację tenantów i ograniczenia na poziomie wiersza.






