Opublikowano 26 sierpnia 2026 · 3 min czytania
Policzyłem, ile tras w moim API jest naprawdę chronionych. Grep po pliku kłamał
Mój panel ma dziś ponad 240 tras API. Każda z nich zaczyna się od tej samej linijki:
if (!(await isAuthed())) return new Response(null, { status: 401 });
Nie ma warstwy, która robiłaby to za mnie. W tym frameworku katalog /api jest domyślnie poza zasięgiem pośrednika, więc cała kontrola dostępu to powtórzenie tej jednej linijki w każdym pliku z osobna. Zapomnienie jej nie daje żadnego objawu: kod się kompiluje, panel działa, a trasa stoi otworem dla każdego, kto zna adres.
Postanowiłem policzyć, ile ich naprawdę jest.
Pierwsza odpowiedź była fałszywa
Odruch: policzyć pliki, które zawierają isAuthed, i porównać z liczbą plików tras. Wyszło ładnie — prawie wszystkie.
Prawie. Zacząłem oglądać te, które „prawie” tworzyły, i trafiłem na coś takiego:
// Trasa PUBLICZNA — celowo bez isAuthed(): otwiera ją klient
// linkiem z maila, więc nie ma tu sesji do sprawdzenia.
Plik wspominał isAuthed. W komentarzu. Uzasadniającym, dlaczego go nie ma.
Takich miejsc było pięć. Każde z nich moje pierwsze liczenie zaliczyło jako „chronione”, bo szukałem ciągu znaków w pliku, a nie odpowiadałem na pytanie, które naprawdę mnie interesowało.
Drugi błąd: plik to nie trasa
Nawet po wyrzuceniu komentarzy liczenie po plikach nie ma sensu, bo jeden plik może obsługiwać kilka metod HTTP. Typowa trasa wygląda tak:
export async function GET() { … }
export async function PATCH(){ … }
export async function DELETE(){ … }
Jeżeli sprawdzenie jest w GET, ale ktoś dopisał później DELETE i zapomniał — plik nadal „zawiera isAuthed”. Wyszukiwarka mówi: chronione. Rzeczywistość: skasować rekord może każdy.
Właściwą jednostką liczenia jest uchwyt HTTP, nie plik. Dopiero taki rachunek odpowiada na pytanie „czy każde wejście do systemu jest zamknięte”.
Jak to policzyłem w końcu
Wypisałem wszystkie eksportowane funkcje GET/POST/PATCH/PUT/DELETE w katalogu tras, a potem dla każdej sprawdziłem, czy sprawdzenie sesji pada w jej ciele, a nie gdziekolwiek w pliku. Do tego osobna lista tras, które są publiczne z założenia — akceptacja oferty przez klienta, odbiór dokumentu z linku, formularz kontaktowy — każda z krótkim uzasadnieniem, dlaczego wolno jej stać otworem.
Efekt jest nudny i o to chodzi: dwie listy, które da się przejrzeć w pięć minut, i pytanie „ile tras jest niechronionych” ma odpowiedź liczbową zamiast wrażenia.
Co z tego wynika ogólniej
Wyszukiwanie tekstu odpowiada na pytanie o tekst, nie o zachowanie. To brzmi banalnie i właśnie dlatego jest groźne: wynik wygląda na pomiar, a jest tylko liczbą trafień. Za każdym razem, gdy z takiego szukania wyciągam wniosek, muszę sobie zadać pytanie, czy to, co policzyłem, jest tym, o co pytałem.
Bezpieczeństwo oparte na powtarzaniu tej samej linijki jest bezpieczeństwem opartym na pamięci. Docelowo to powinna być bariera, przez którą nie da się przejść przez zapomnienie. Na dziś mam listę i test, który ją sprawdza — świadomie mniej niż bariera, ale sporo więcej niż nadzieja.
Publiczne bywa poprawne — pod warunkiem, że jest napisane. Pięć tras stoi otworem celowo. Różnica między świadomym wyjątkiem a dziurą polega wyłącznie na tym, że ktoś zapisał, który to który. Nieudokumentowany wyjątek po pół roku jest nie do odróżnienia od pomyłki — nawet dla autora.
To był fragment przeglądu, który zrobiłem sobie sam, na własnym systemie, bez zewnętrznego audytora. Nie twierdzę, że jest kompletny — twierdzę, że po nim wiem, ile tras mam i które z nich są otwarte, a przed nim tylko mi się wydawało.

Patryk Piecyk
Warszawa · junior konsultant wdrożeniowy · dostępny od zaraz
Przez 7,5 roku pracowałem w niemieckiej firmie, pięć lat prowadząc jej biuro: zamówienia, faktury, reklamacje, ERP. Od czerwca 2026 buduję własne narzędzia do tej samej roboty — nie jestem programistą z wykształcenia, kod powstaje w parze z asystentem AI, a projekt, decyzje i testy są moje. Te notatki opisują, co się w tych systemach zepsuło i co z tego wyszło.
Masz pytanie do tego tekstu?
Napisz do mnie →