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

SQL injection w erze ORM: gdzie nadal powstaje

Parametryzacja nie chroni identyfikatorów, a ORM ma furtki. Gdzie SQLi żyje dziś: sortowanie, raporty, warianty ślepe i drugorzędne. Obrona i detekcja.

MATERIAŁ PUBLICZNY
AUTOR
/ CEO Breachroad · OSCP · PNPT
PUBLIKACJA
25 lipca 2026
CZAS CZYTANIA
18 min czytania
TEMAT
Pentest i AppSec
SQL injection w erze ORM: gdzie nadal powstaje

SQL injection miała zniknąć razem z upowszechnieniem map obiektowo-relacyjnych i zapytań parametryzowanych. Nie zniknęła — zmieniła adres. Dziś prawie nigdy nie znajdziesz jej w podstawowych operacjach na encjach, bo te przechodzą przez bezpieczne API. Znajdziesz ją tam, gdzie ktoś musiał zbudować zapytanie, którego API nie obsługiwało: w dynamicznym sortowaniu, w filtrach raportów, w wyszukiwarkach z wieloma kryteriami, w eksportach, w panelu administracyjnym, w zadaniach wsadowych i w procedurach składowanych napisanych dziesięć lat temu.

Zrozumienie, dlaczego tak jest, wymaga jednego rozróżnienia, które przesądza o całej reszcie.

Parametr wiąże wartość, nie strukturę

Zapytanie parametryzowane działa dlatego, że baza dostaje osobno tekst zapytania i osobno dane. Silnik parsuje strukturę raz, a wartości podstawia w miejsca oznaczone, nie interpretując ich jako składni. To skuteczne i praktycznie nieprzełamywalne.

Ograniczenie polega na tym, że parametr może reprezentować wyłącznie wartość. Nie może być nazwą tabeli, nazwą kolumny, kierunkiem sortowania, operatorem porównania ani fragmentem klauzuli. Kiedy aplikacja musi zbudować zapytanie, w którym zmienna jest właśnie taka, parametryzacja nie ma zastosowania — i dokładnie w tym miejscu programista sięga po sklejanie napisów.

Wniosek praktyczny: każde dynamiczne sortowanie i filtrowanie jest kandydatem na podatność, dopóki nie zobaczysz w kodzie mapy przekształcającej wejście użytkownika na stałą wartość zdefiniowaną w aplikacji. Nie listy zakazanych znaków — mapy. Wejście data_utworzenia ma się przekładać na literalną nazwę kolumny obecną w kodzie, a wszystko spoza mapy ma kończyć się odrzuceniem żądania.

Gdzie ORM przecieka

Mapa obiektowo-relacyjna nie jest granicą bezpieczeństwa, tylko wygodą, a każda z nich ma udokumentowane wyjścia awaryjne. To właśnie w nich mieszka współczesna SQLi.

Metody wykonujące surowe zapytania są oczywistym kandydatem, ale nie jedynym. Groźniejsze są fragmenty surowego SQL wstawiane w środek bezpiecznego zapytania — warunek w filtrze, wyrażenie w klauzuli sortowania, fragment w złączeniu. Wyglądają niewinnie, bo otacza je kod korzystający z API, więc podczas przeglądu łatwo je przeoczyć.

Osobną kategorią są języki zapytań warstwy obiektowej. Wstrzyknięcie do zapytania w języku ORM-a bywa mniej efektowne od klasycznego, ale nadal pozwala odczytać dane spoza zamierzonego zakresu, zwłaszcza przez manipulację warunkami.

Warto też pamiętać o dopasowaniu wzorców: wartość przekazana do operatora dopasowania jest parametrem, więc nie prowadzi do wstrzyknięcia SQL, ale znaki wieloznaczne w niej zawarte zmieniają wynik zapytania. Bez ich obsługi użytkownik potrafi wymusić skanowanie całej tabeli albo pobrać rekordy, których nie powinien widzieć w wynikach wyszukiwania. To nie jest SQLi, ale bywa mylone i również wymaga obsługi.

Podobnie ścieżki dostępu do danych częściowo ustrukturyzowanych: wyrażenia adresujące elementy dokumentu w kolumnie typu JSON bywają budowane przez konkatenację, bo sterownik nie przewiduje dla nich parametru.

Warianty drugorzędne

Najtrudniejsza do znalezienia odmiana nie objawia się w miejscu wprowadzenia danych. Wartość jest zapisywana poprawnie, przez zapytanie parametryzowane, i przez jakiś czas nie robi nic złego. Później sięga po nią inny komponent — generator raportu, zadanie nocne, procedura składowana, migracja, integracja — i buduje z niej zapytanie przez sklejanie napisów.

Ta odmiana przechodzi przez testy skanerem, ponieważ skaner obserwuje odpowiedź na żądanie, a skutek pojawia się godzinę później w innym systemie. Znaleźć ją można wyłącznie przez śledzenie cyklu życia danych: gdzie ta wartość jest później czytana i przez jaki kod.

Z tego samego powodu procedury składowane nie są zabezpieczeniem. Procedura, która wewnątrz buduje zapytanie z konkatenacji, jest podatna dokładnie tak samo, a często wykonuje się z wyższymi uprawnieniami niż konto aplikacji, więc skutki są większe.

Warianty ślepe

Brak komunikatu błędu i brak danych w odpowiedzi nie oznacza braku podatności. Trzy odmiany działają bez echa.

W wariancie logicznym atakujący wnioskuje z tego, czy odpowiedź aplikacji wygląda jak dla warunku prawdziwego, czy fałszywego — wystarczy różnica w liczbie wyników albo w treści komunikatu.

W wariancie czasowym wnioskowanie opiera się na czasie odpowiedzi, wymuszonym operacją opóźniającą po stronie bazy. Działa nawet wtedy, gdy aplikacja zwraca zawsze tę samą stronę błędu.

W wariancie poza pasmem baza wykonuje operację sieciową — rozwiązanie nazwy albo żądanie — a potwierdzeniem jest obserwacja tego ruchu. Ten wariant bywa jedyną drogą wykrycia w zapytaniach wykonywanych asynchronicznie.

Dla obrońcy płynie stąd konkretny wniosek: cisza w odpowiedzi nie jest dowodem bezpieczeństwa, a limit czasu zapytania i blokada ruchu wychodzącego z serwera bazy odbierają dwa z trzech kanałów.

Skutki wykraczające poza wyciek

Wyciek danych jest najczęściej wymienianym skutkiem, ale nie jedynym i nie zawsze najgorszym.

Obejście autoryzacji: wstrzyknięcie w warunek filtrujący pozwala zwrócić rekordy innych podmiotów, co w aplikacji wielonajemcowej jest równoważne przełamaniu izolacji.

Modyfikacja danych: jeżeli konto bazy ma prawa zapisu tam, gdzie ich nie potrzebuje, podatność w zapytaniu odczytującym potrafi zmienić stan systemu.

Eskalacja w infrastrukturze: przy nadmiernych uprawnieniach konta lub włączonych rozszerzeniach silnika możliwe jest sięgnięcie do systemu plików serwera bazy, a stamtąd dalej. To zależy od konkretnego silnika i konfiguracji, ale jest realnym scenariuszem w środowiskach, które nigdy nie ograniczyły uprawnień.

Odmowa usługi: wymuszenie zapytania o wysokiej złożoności potrafi wysycić bazę szybciej niż jakikolwiek atak wolumetryczny.

Obrona warstwowa

Parametryzacja bez wyjątków. Także dla danych „zaufanych”, pochodzących z innego systemu, z konfiguracji albo z bazy. Wyjątek zrobiony dla wartości wewnętrznej jest dokładnie tym miejscem, w którym powstaje wariant drugorzędny.

Allowlist dla identyfikatorów. Nazwy kolumn, tabel, kierunek sortowania i operator porównania pochodzą ze słownika w kodzie. Wejście użytkownika jest kluczem do słownika, nigdy jego wartością.

Bezpieczne budowanie zapytań. Jeżeli aplikacja wymaga złożonego, dynamicznego SQL, warto zbudować własną warstwę, która przyjmuje strukturę filtrów i sama generuje zapytanie z parametrami — zamiast pozwalać każdemu programiście sklejać napisy w kontrolerze. Taka warstwa jest testowalna i podlega jednemu przeglądowi zamiast trzydziestu.

Minimalne uprawnienia konta bazy. Osobne konto na usługę, tylko potrzebne operacje, brak praw do zmiany schematu, brak dostępu do widoków systemowych i katalogów metadanych, brak uprawnień administracyjnych. To nie zapobiega wstrzyknięciu, ale drastycznie zmniejsza jego skutki — różnica między odczytem jednej tabeli a odczytem całej instancji.

Limity. Limit czasu wykonania zapytania, limit liczby zwracanych wierszy na poziomie warstwy dostępu, limit rozmiaru odpowiedzi. Ograniczają zarówno warianty czasowe, jak i masowe pobieranie danych.

Komunikaty błędów. Szczegóły błędów bazy nie mogą trafiać do odpowiedzi. Trafiają za to do logów, gdzie są cenne — o czym za chwilę.

Analiza statyczna i przegląd. Reguła wykrywająca konkatenację lub interpolację w kontekście zapytania ma bardzo dobry stosunek trafień do fałszywych alarmów. Warto ją włączyć i traktować każde trafienie jako wymagające uzasadnienia.

Zapora aplikacyjna jest opóźnieniem, nie naprawą. Reguły brzegowe podnoszą koszt automatycznego skanowania i bywają użyteczne jako sygnał detekcyjny, ale nie zmieniają faktu, że aplikacja składa zapytanie z danych.

Metodyka testu

Test zaczyna się od inwentaryzacji punktów wejścia, przy czym lista jest szersza niż pola formularzy: parametry zapytania, ciało żądania, nagłówki, ciasteczka, nazwy plików, dane z innych systemów oraz wartości pochodzące z importów. Każde miejsce, w którym dane trafiają do zapytania, jest kandydatem.

Drugi krok to przegląd kodu, jeśli jest dostępny. Odnalezienie miejsc z surowym SQL zajmuje minuty i daje pewność, której testowanie z zewnątrz nie da. W testach z pełną wiedzą to zawsze najlepsze wykorzystanie czasu.

Trzeci krok to obserwacja różnic w zachowaniu przy zmianie danych wejściowych: różnice w liczbie wyników, w treści komunikatów, w kodach odpowiedzi i w czasie. Wnioskowanie prowadzimy metodycznie, notując parę „wejście — obserwacja”, bo w wariantach ślepych to jedyny sposób na powtarzalność.

Czwarty krok to potwierdzenie minimalnym dowodem. Do wykazania podatności wystarczy pokazać, że warunek logiczny w zapytaniu daje się zmienić — nie trzeba pobierać zawartości tabel. Masowe wyciąganie danych z systemu klienta jest zbędne, ryzykowne prawnie i czasami niszczące.

Piąty krok, o którym trzeba pamiętać na produkcji: część technik potrafi zmodyfikować lub uszkodzić dane. Testy potwierdzające należy wykonywać na środowisku z odtwarzalną kopią, a na produkcji ograniczać się do obserwacji bez skutków ubocznych — i mieć to uzgodnione na piśmie w zakresie testu.

Detekcja

Najlepszym sygnałem detekcyjnym są błędy składni SQL w logach aplikacji. Poprawnie działająca aplikacja nie generuje ich w ogóle, więc każde wystąpienie oznacza albo błąd w kodzie, albo próbę wstrzyknięcia. To reguła o niemal zerowym poziomie szumu, a mimo to rzadko wdrażana.

Drugim sygnałem jest czas wykonania zapytań. Zapytanie trwające wielokrotnie dłużej niż zwykle w endpointcie o stałej charakterystyce wskazuje na wariant czasowy.

Trzecim jest liczba zwracanych wierszy per endpoint. Punkt zwracający zwykle kilkanaście rekordów, który nagle zwraca dziesiątki tysięcy, jest wart alertu niezależnie od przyczyny.

Czwartym jest dostęp do widoków systemowych i katalogów metadanych przez konto aplikacyjne. W poprawnie skonfigurowanym środowisku konto nie ma do nich uprawnień, ale samo odnotowanie prób jest cenne.

Piątym — ruch wychodzący z serwera bazy danych, jeżeli architektura go nie przewiduje.

Dług w starym kodzie

W bazie kodu, która ma dziesięć lat i setki zapytań, przepisanie wszystkiego naraz nie jest realne. Sensowna kolejność wygląda tak: najpierw zapytania osiągalne bez uwierzytelnienia, potem zapytania wykonywane kontem o szerokich uprawnieniach, potem te w ścieżkach administracyjnych, a na końcu reszta.

Równolegle warto odciąć skutki, nawet zanim naprawi się przyczynę: ograniczyć uprawnienia konta bazy, wprowadzić limity wierszy i czasu, wyciszyć błędy w odpowiedziach i włączyć detekcję opisaną wyżej. Te zmiany są tanie, nie wymagają dotykania logiki i zmieniają potencjalny incydent krytyczny w incydent poważny — a to realna różnica.

Ta sama klasa błędu w innych interpreterach

Warto zauważyć, że SQL jest tylko najbardziej znanym przypadkiem ogólniejszego wzorca: dane od użytkownika trafiają do interpretera, który odróżnia składnię od wartości. Ten sam błąd występuje wszędzie tam, gdzie jest jakikolwiek język zapytań.

W bazach dokumentowych klasycznym przypadkiem jest przekazanie do zapytania struktury zamiast wartości skalarnej. Jeżeli warstwa aplikacji przyjmuje z żądania obiekt i wstawia go bezpośrednio jako warunek, użytkownik może podstawić operator porównania zamiast wartości i zmienić sens zapytania — na przykład zamienić sprawdzenie równości hasła na warunek zawsze prawdziwy. Obrona polega na wymuszeniu typu: pole, które ma być napisem, musi być napisem, a nie strukturą.

W katalogach LDAP wstrzyknięcie do filtru pozwala poszerzyć zakres wyszukiwania i obejść warunki ograniczające. Ponieważ katalogi bywają źródłem decyzji uwierzytelniających, skutki są analogiczne do obejścia autoryzacji.

W zapytaniach po dokumentach XML manipulacja wyrażeniem ścieżki pozwala odczytać węzły spoza zamierzonego zakresu. To pokrewne rodzinie problemów z parserami XML i występuje w tych samych integracjach.

We wszystkich tych przypadkach obowiązuje ta sama zasada, co przy SQL: wartości przekazujemy mechanizmem przewidzianym przez bibliotekę, a strukturę budujemy z elementów zdefiniowanych w kodzie. Jeżeli biblioteka nie oferuje parametryzacji, to jej brak jest argumentem przeciw tej bibliotece, a nie zaproszeniem do sklejania napisów.

Checklista

Wszystkie wartości trafiają do zapytań jako parametry, bez wyjątków dla danych wewnętrznych. Identyfikatory i kierunki sortowania pochodzą ze słownika w kodzie. Nie ma konkatenacji w warstwie dostępu do danych, a wyjątki są uzasadnione i przejrzane. Procedury składowane nie budują zapytań z konkatenacji. Konto bazy ma minimalne uprawnienia i brak dostępu do katalogów systemowych. Obowiązują limity czasu zapytania i liczby wierszy. Błędy bazy nie trafiają do odpowiedzi, ale trafiają do logów. Serwer bazy nie ma ruchu wychodzącego. Analiza statyczna wykrywa konkatenację w zapytaniach. Cykl życia danych jest przeanalizowany pod kątem wariantów drugorzędnych.

Wniosek

SQL injection nie jest problemem rozwiązanym, tylko problemem przesuniętym. Warstwa dostępu do danych w typowej aplikacji jest dziś bezpieczna, ale wyjątki od niej nie zniknęły — przeniosły się do sortowania, raportów, zadań wsadowych i starego kodu, którego nikt nie chce ruszać.

Praktyczny wniosek jest dwuczęściowy. Po pierwsze, szukaj tam, gdzie parametryzacja nie działa z definicji: przy identyfikatorach i strukturze zapytania. Po drugie, projektuj tak, żeby skutki były ograniczone niezależnie od tego, czy gdzieś jednak został wyjątek — minimalne uprawnienia konta bazy i limity wierszy kosztują jedno popołudnie, a decydują o tym, czy incydent obejmie jedną tabelę, czy całą instancję.


Źródła pierwotne: OWASP — SQL Injection Prevention Cheat Sheet, CWE-89: SQL Injection, CWE-564: SQL Injection: Hibernate.

UDOSTĘPNIJ / KOPIUJ