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

IDOR i BOLA: testowanie autoryzacji obiektów

Dlaczego autoryzacja na poziomie obiektu psuje się najczęściej: warianty IDOR, mity o UUID, wzorce naprawy, metodyka testu i detekcja w logach.

MATERIAŁ PUBLICZNY
AUTOR
/ CEO Breachroad · OSCP · PNPT
PUBLIKACJA
25 lipca 2026
CZAS CZYTANIA
18 min czytania
TEMAT
Pentest i AppSec
IDOR i BOLA: testowanie autoryzacji obiektów

Najczęstsza podatność, jaką znajduję w API, nie jest wyrafinowana. Aplikacja poprawnie sprawdza, kto wysłał żądanie, po czym zwraca obiekt, o który poproszono, nie pytając, czy ten konkretny użytkownik ma do niego prawo. To jest IDOR — insecure direct object reference — a w nazewnictwie OWASP API Security BOLA, czyli broken object level authorization. Od lat zajmuje pierwsze miejsce na liście ryzyk API i nic nie wskazuje, żeby miało je oddać.

Powód, dla którego ta klasa błędów jest tak trwała, jest strukturalny. Uwierzytelnienie da się scentralizować w jednym middleware. Autoryzacja funkcyjna („czy rola administratora może wywołać ten endpoint”) też, bo zależy tylko od roli i ścieżki. Autoryzacja obiektowa wymaga natomiast wiedzy o modelu własności danych i musi być podjęta osobno przy każdym obiekcie w każdej ścieżce. Jest więc rozproszona po całej aplikacji i psuje się dokładnie tam, gdzie nikt nie patrzył.

Trzy różne pytania

Warto rozdzielić trzy pytania, które w kodzie bywają mieszane.

Pierwsze: kim jesteś? To uwierzytelnienie. Odpowiada za nie sesja albo token i zwykle działa poprawnie, bo jest jedno i widoczne.

Drugie: czy twoja rola może wykonać tę operację? To autoryzacja funkcyjna, w OWASP opisana jako BFLA. Tu błędy polegają na ukrywaniu przycisku zamiast blokowania endpointu.

Trzecie: czy możesz wykonać tę operację na tym konkretnym obiekcie? To autoryzacja obiektowa. Poprawna odpowiedź wymaga jednoczesnego rozważenia podmiotu, obiektu i czynności. Zdanie „użytkownik może pobrać fakturę” nie jest polityką. Polityką jest „użytkownik może pobrać fakturę, której jest właścicielem lub która należy do jego organizacji, jeśli nie została oznaczona jako ukryta”.

Większość podatności IDOR to sytuacje, w których kod odpowiedział na pierwsze i drugie pytanie, a trzecie pominął.

Cztery mity, które utrzymują tę podatność przy życiu

Mit pierwszy: UUID wystarczy. Identyfikator losowy utrudnia zgadywanie, ale nie jest kontrolą dostępu. Identyfikatory wyciekają masowo i całkowicie legalnie: w listach zwracanych innym użytkownikom, w eksportach CSV, w treści e-maili, w nagłówku Referer, w logach proxy, w linkach udostępnianych na czacie, w systemach zgłoszeń. Kiedy identyfikator już wycieknie — a wycieknie — jedyną barierą pozostaje sprawdzenie po stronie serwera. Losowość zmienia klasę ataku z enumeracji na wykorzystanie pojedynczej referencji, i to jest cała różnica.

Mit drugi: interfejs nie pokazuje tej opcji. Interfejs jest podpowiedzią, nie granicą. Każdy endpoint jest osiągalny bezpośrednio, a atakujący nie korzysta z waszego frontendu.

Mit trzeci: identyfikator jest zaszyfrowany. Zwykle nie jest zaszyfrowany, tylko zakodowany albo zahaszowany w przewidywalny sposób. Nawet jeśli faktycznie jest szyfrowany, użytkownik dostaje przecież kryptogramy swoich obiektów, a jeśli gdziekolwiek zobaczy cudzy — wraca punkt wyjścia. Obfuskacja referencji bywa użytecznym dodatkiem, nigdy zamiennikiem kontroli.

Mit czwarty: tenant jest w tokenie. Bardzo dobrze, tylko czy zapytanie do bazy zawiera warunek na ten tenant? W wielu wdrożeniach identyfikator organizacji jest starannie weryfikowany w tokenie i nigdy nie trafia do klauzuli WHERE. Token wie, kim jesteś. Zapytanie i tak zwraca cudzy wiersz.

Warianty, które łatwo przeoczyć

Podstawowy przypadek — podmiana identyfikatora w ścieżce zasobu — jest zwykle testowany. Ciekawe rzeczy dzieją się poza nim.

Metody poza odczytem. Zdarza się, że GET /orders/{id} jest zabezpieczony, a PATCH na tym samym zasobie już nie, bo trafił do innego kontrolera napisanego przez inny zespół. Testujemy każdą metodę osobno, nie tylko odczyt.

Identyfikatory w ciele żądania i w polach zagnieżdżonych. Referencja do obiektu bywa schowana głęboko: identyfikator adresu dostawy, identyfikator metody płatności, identyfikator załącznika w tablicy. Kontroler waliduje obiekt główny i przepisuje pola potomne bez sprawdzenia właściciela.

Operacje masowe. Endpoint przyjmujący listę identyfikatorów do usunięcia lub eksportu bywa sprawdzany tylko dla pierwszego elementu albo tylko na poziomie „czy użytkownik ma prawo do operacji masowej”.

Eksporty, raporty i podglądy. Ścieżki generujące PDF, wysyłające dokument mailem albo tworzące podgląd potrafią pomijać kontrolę, bo powstały jako „funkcja wewnętrzna” i dopiero później zostały wystawione.

Pliki i załączniki. Warstwa plikowa często żyje obok warstwy danych. Obiekt jest chroniony, a plik leży pod przewidywalnym adresem w magazynie obiektowym albo pod podpisanym URL-em o zbyt długiej ważności i zbyt szerokim zakresie.

GraphQL. Pojedynczy węzeł pobierany po globalnym identyfikatorze oraz relacje zagnieżdżone tworzą wiele ścieżek do tego samego obiektu. Kontrola założona na jednym resolverze nie chroni pozostałych.

Przypisanie właściciela przy tworzeniu. Jeżeli żądanie tworzące zasób przyjmuje pole ownerId lub organizationId i przepisuje je bez weryfikacji, atakujący nie czyta cudzych danych — wstawia własne obiekty do cudzej organizacji. To bywa groźniejsze niż odczyt, bo tworzy trwałą pozycję w systemie ofiary.

Procesy wieloetapowe. Kontrola bywa w kroku pierwszym, a stan przenoszony do kroku drugiego identyfikatorem, który już nikt nie waliduje.

Wzorzec naprawy: autoryzacja bliżej danych

Najtrwalszą naprawą nie jest dopisanie warunku w kontrolerze, bo ten warunek trzeba potem dopisywać w każdym nowym kontrolerze. Trwała naprawa polega na tym, żeby niemożliwe stało się pobranie obiektu bez kontekstu podmiotu.

W praktyce oznacza to warstwę dostępu do danych, która nie udostępnia metody „pobierz po identyfikatorze”. Zamiast niej istnieje metoda „pobierz po identyfikatorze dla tego podmiotu”, a zapytanie zawsze zawiera warunek własności lub przynależności do organizacji. Kod, który chce obejść tę zasadę, musi zrobić to jawnie i taki wyjątek jest widoczny w przeglądzie kodu.

Drugim filarem jest centralna decyzja polityki. Reguły opisane w jednym miejscu — jako funkcje polityk, silnik reguł albo model uprawnień oparty na relacjach — dają się testować i audytować. Reguły rozsiane po kontrolerach nie dają się nawet zinwentaryzować.

Trzecim, bardzo skutecznym elementem jest zabezpieczenie w bazie danych: mechanizm bezpieczeństwa na poziomie wiersza, który filtruje dane po identyfikatorze najemcy niezależnie od tego, co zrobiła aplikacja. To siatka bezpieczeństwa na wypadek błędu w kodzie, a nie zamiennik kontroli aplikacyjnej.

Warto też uważać na kolejność operacji. Wzorzec „pobierz obiekt, wykonaj efekt uboczny, potem sprawdź uprawnienia” bywa spotykany w kodzie asynchronicznym i kolejkowym. Sprawdzenie musi poprzedzać każdy efekt uboczny, łącznie z wysłaniem powiadomienia, które samo w sobie potrafi ujawnić dane.

Metodyka testu

Test autoryzacji obiektowej wymaga przygotowania, którego skaner nie wykona za nas, ponieważ narzędzie nie wie, kto jest właścicielem czego.

Konta. Potrzebne są minimum dwa konta w każdej istotnej roli oraz dwie odrębne organizacje. Konto A i konto B w tej samej organizacji wykrywają błędy własności. Organizacja X i Y wykrywają błędy izolacji najemców. Bez tej macierzy część wyników jest nierozstrzygalna.

Inwentaryzacja. Zbierz listę endpointów wraz z metodami i miejscami, w których pojawiają się referencje do obiektów — ścieżka, ciało, parametry zapytania, nagłówki. Specyfikacja OpenAPI jest dobrym punktem startu, ale trzeba ją zweryfikować z ruchem rzeczywistym, bo najciekawsze ścieżki bywają nieudokumentowane.

Macierz przypadków. Dla każdej pary endpoint–metoda wykonaj to samo żądanie kontem A na obiekcie konta B, kontem z organizacji X na obiekcie organizacji Y oraz kontem o niższej roli na obiekcie wymagającym wyższej. Wynik zapisuj jako kod odpowiedzi, rozmiar i istotny fragment treści.

Różnice, nie same kody. Odpowiedź 200 z pustą listą, 200 z częściowymi danymi i 403 to trzy różne stany. Bywa, że obiekt nie jest zwracany, ale operacja i tak została wykonana — dlatego przy metodach modyfikujących sprawdzamy skutek uboczny na koncie ofiary, a nie treść odpowiedzi.

Rozróżnienie 403 od 404. Zwracanie 404 dla cudzych obiektów ogranicza enumerację, ale utrudnia diagnozę. Ważniejsze jest, aby aplikacja była konsekwentna: jeśli dla nieistniejącego obiektu zwraca 404, a dla cudzego 403, potwierdza istnienie zasobów, których użytkownik nie powinien widzieć. Sama ta różnica jest już wyciekiem informacji.

Dowód minimalny. W raporcie z testu ogranicz dowód do tego, co konieczne — identyfikator obiektu i pojedyncze pole potwierdzające cudzą własność. Zrzuty pełnych rekordów z danymi osobowymi tworzą kolejny problem, tym razem po stronie zleceniodawcy.

Praca ręczna szybko się wyczerpuje, więc warto ją zautomatyzować: skrypt powtarzający zebrany ruch konta A z tokenem konta B i porównujący odpowiedzi wykrywa większość przypadków, a różnice wymagające oceny człowieka są wtedy krótką listą.

Detekcja i telemetria

IDOR jest wykrywalny w logach, jeżeli logi zawierają właściwe pola. Minimalny zestaw to: identyfikator podmiotu, identyfikator organizacji, typ i identyfikator obiektu, czynność oraz wynik decyzji autoryzacyjnej. Sam kod HTTP nie wystarczy, bo nie odróżnia „obiekt nie istnieje” od „odmówiono dostępu”.

Na takich danych działają proste, skuteczne reguły. Gwałtowny wzrost odmów dla jednego podmiotu wskazuje na enumerację. Dostęp do liczby unikalnych obiektów znacząco odbiegającej od typowej dla roli wskazuje na zbieranie danych. Odwołania do identyfikatorów spoza organizacji podmiotu powinny być alertem niezależnie od tego, czy zakończyły się sukcesem — udana odmowa też jest informacją o próbie. Sekwencyjne identyfikatory w kolejnych żądaniach to klasyczny wzorzec skanowania.

Warto też mierzyć stosunek sukcesów do odmów per endpoint. Endpoint, na którym odmowy praktycznie nie występują, albo jest dobrze zaprojektowany, albo nie sprawdza niczego — i to drugie da się szybko potwierdzić testem.

Zapobieganie regresji

Autoryzacja obiektowa psuje się przy zmianach, nie przy pierwszym wdrożeniu. Nowy endpoint, refaktoryzacja repozytorium, dodanie relacji w modelu — każda z tych zmian potrafi otworzyć ścieżkę omijającą kontrolę.

Praktyka, która działa, jest jedna: każdy endpoint zwracający lub modyfikujący obiekt ma test negatywny, uruchamiany w CI, w którym podmiot bez uprawnień otrzymuje odmowę. Test kosztuje kilka minut przy pisaniu i wykrywa całą klasę regresji. Uzupełnieniem jest przegląd kodu z jednym pytaniem kontrolnym: skąd ten kod wie, że użytkownik ma prawo do tego obiektu.

Udostępnianie, zaproszenia i dostęp czasowy

Osobnej uwagi wymagają funkcje, które celowo poszerzają dostęp: linki do udostępniania, zaproszenia do organizacji, dostęp gościnny, podgląd dla klienta, delegacja na czas nieobecności. Każda z nich tworzy drugą ścieżkę autoryzacji obok podstawowej, a druga ścieżka bywa pisana szybciej i przeglądana rzadziej.

Typowe błędy w tej grupie są powtarzalne. Link do udostępniania nadaje uprawnienie do obiektu, ale nie ogranicza czynności — odbiorca miał tylko oglądać, a może modyfikować. Zaproszenie do organizacji jest realizowane przez identyfikator, który da się podmienić, więc zaproszony trafia do innej organizacji niż zamierzona. Dostęp czasowy jest przyznawany, ale nie wygasa, ponieważ wygaśnięcie sprawdza się tylko przy wyświetlaniu listy, nie przy pobraniu obiektu. Odebranie dostępu usuwa wpis w interfejsie, lecz nie unieważnia wcześniej wygenerowanego linku.

Reguła projektowa jest tu prosta: udostępnienie to obiekt w modelu danych, a nie wyjątek w kodzie. Ma właściciela, zakres czynności, termin ważności, historię użycia i możliwość odwołania. Sprawdzenie tego obiektu odbywa się w tym samym miejscu, co sprawdzenie własności — czyli w warstwie dostępu do danych, nie w kontrolerze, który akurat obsługuje udostępnianie.

Testowo warto pokryć cztery scenariusze: użycie linku po terminie, użycie linku po odebraniu dostępu, próba czynności wykraczającej poza zakres udostępnienia oraz próba użycia linku przez konto z innej organizacji. Każdy z nich odpowiada realnemu incydentowi, który zdarzył się już wielu firmom.

Checklista

Każda referencja do obiektu jest autoryzowana po stronie serwera, niezależnie od interfejsu. Warstwa dostępu do danych nie udostępnia pobierania po samym identyfikatorze. Zapytania zawierają warunek najemcy, a baza ma zabezpieczenie na poziomie wiersza jako siatkę bezpieczeństwa. Pola ownerId i organizationId z żądania są ignorowane lub weryfikowane, nigdy przepisywane. Operacje masowe autoryzują każdy element listy. Pliki i podpisane adresy mają krótką ważność i wąski zakres. Metody modyfikujące są testowane osobno od odczytu. Odpowiedzi na dostęp nieuprawniony są spójne. Logi zawierają podmiot, obiekt, czynność i decyzję. Każdy endpoint ma test negatywny w CI.

Wniosek

BOLA utrzymuje się na szczycie list ryzyk nie dlatego, że jest trudna do naprawienia, ale dlatego, że jest łatwa do pominięcia w jednym miejscu na sto. Skanery jej nie widzą, bo nie znają modelu własności. Testy jednostkowe jej nie widzą, bo zwykle sprawdzają ścieżkę pozytywną. Przegląd kodu jej nie widzi, gdy patrzy na kontroler bez kontekstu zapytania.

Rozwiązanie polega na przesunięciu kontroli w miejsce, którego nie da się przypadkiem ominąć: do warstwy danych, do centralnej polityki i do bazy. Wszystko pozostałe — testy, telemetria, konsekwentne kody odpowiedzi — służy do wykrycia sytuacji, w której ktoś jednak to obszedł.


Źródła pierwotne: OWASP API Security Top 10 2023 — API1:2023 Broken Object Level Authorization, CWE-639: Authorization Bypass Through User-Controlled Key, OWASP — Authorization Cheat Sheet.

UDOSTĘPNIJ / KOPIUJ