← Wszystkie artykuły

Czym jest atak SQL injection i jak się przed nim chronić?

Burncode Team 4 minuty czytania

SQL injection to znana i dobrze opisana podatność od ponad dwudziestu lat, a mimo to co roku pojawia się w prawdziwych wyciekach danych – nie dlatego, że trudno jej zapobiec, ale dlatego, że łatwo wprowadzić ją przypadkiem, zwłaszcza w starszym kodzie albo w kodzie pisanym pod presją terminu.

Czym właściwie jest SQL injection

Większość aplikacji webowych przechowuje dane w bazie danych i odczytuje je oraz zapisuje za pomocą języka zapytań SQL. Do SQL injection (wstrzyknięcia SQL) dochodzi, gdy aplikacja bierze tekst wpisany przez użytkownika – w polu wyszukiwania, formularzu logowania, parametrze URL – i wstawia go bezpośrednio do zapytania do bazy danych, nie traktując go wcześniej jako niezaufanych danych wejściowych. Jeśli atakujący wpisze coś, co wygląda jak kod SQL zamiast zwykłej wartości, a aplikacja bezrefleksyjnie dołączy to do zapytania, baza danych wykona polecenia napisane przez atakującego, a nie tylko te, które zamierzyła aplikacja.

Prosty przykład

Wyobraźcie sobie sprawdzanie logowania zbudowane w ten sposób, opisując rzecz po ludzku: „znajdź użytkownika, którego nazwa jest równa [temu, co wpisał użytkownik], a hasło jest równe [temu, co wpisał użytkownik]”. Jeśli aplikacja buduje to zapytanie, bezpośrednio doklejając dane od użytkownika do tekstu SQL, atakujący może wpisać w pole nazwy użytkownika coś sprytnego, co zmieni faktyczne znaczenie zapytania – na przykład sprawi, że warunek będzie zawsze prawdziwy niezależnie od hasła. W ten sposób może zalogować się jako dowolny użytkownik, w ogóle nie znając jego hasła.

To prosty przypadek. Bardziej zaawansowane ataki SQL injection pozwalają odczytać dane, których użytkownik nigdy nie powinien zobaczyć – dane osobowe innych użytkowników, dane płatnicze, hashe haseł – a w najgorszym razie wprost modyfikować lub usuwać dane.

Dlaczego wciąż jest tak powszechny

  • Jest niewidoczny, dopóki ktoś go nie przetestuje. Podatny formularz działa bez zarzutu dla każdego zwykłego użytkownika. Problem ujawnia się dopiero wtedy, gdy ktoś celowo przetestuje go złośliwymi danymi, co podczas normalnego developmentu rzadko zdarza się przypadkiem.
  • Kryje się w kodzie pisanym „tylko ten jeden raz”. Szybki skrypt administracyjny, jednorazowe zapytanie do raportu, funkcja dodana w pośpiechu – to dokładnie te miejsca, w których programiści najczęściej budują zapytanie przez bezpośrednie łączenie ciągów znaków zamiast bezpiecznego wzorca, bo w danej chwili wydaje się to szybsze.
  • Do starego kodu nikt nie wraca. Wzorzec wprowadzony lata temu w rzadko dotykanej części kodu może tkwić tam po cichu, dopóki ktoś wreszcie nie zacznie go szukać.

Jak naprawdę mu zapobiegać

Rozwiązaniem nie jest mgliste hasło „sanityzujcie dane wejściowe”, tylko konkretna, dobrze ugruntowana technika, która przy konsekwentnym stosowaniu praktycznie eliminuje problem.

  • Zawsze używajcie zapytań parametryzowanych (prepared statements). Zamiast budować zapytanie przez sklejanie ciągów znaków, pisze się je z symbolami zastępczymi, a wartości od użytkownika przekazuje osobno. Baza danych traktuje wtedy te wartości wyłącznie jako dane, nigdy jako część polecenia – bez względu na to, jakie znaki zawierają. Praktycznie każda nowoczesna biblioteka bazodanowa to obsługuje i powinno to być ustawienie domyślne, a nie wyjątek stosowany tylko wtedy, gdy ktoś o tym pamięta.
  • Korzystajcie z ORM-a lub query buildera – i róbcie to poprawnie. Narzędzia takie jak Eloquent w Laravelu domyślnie budują pod spodem zapytania parametryzowane. Ryzyko wraca właśnie wtedy, gdy programista sięga po surowe zapytanie, żeby zrobić coś, czego narzędzie nie obsługuje w prosty sposób – to dokładnie to miejsce, w którym trzeba zwolnić i wszystko dwa razy sprawdzić.
  • Stosujcie zasadę najmniejszych uprawnień wobec kont bazodanowych. Użytkownik bazy danych, przez którego łączy się aplikacja, powinien mieć tylko te uprawnienia, których faktycznie potrzebuje. Jeśli jakieś zapytanie zostanie mimo wszystko wykorzystane, ogranicza to możliwości atakującego.
  • Walidujcie typy danych wejściowych jako drugą warstwę, a nie główną obronę. Jeśli pole ma zawierać liczbę, naprawdę wymuście, by była to liczba, zanim trafi do zapytania. Samo w sobie nie wyłapie to wszystkiego, ale zawęża pole możliwych ataków i wcześnie wychwytuje oczywiste błędy.

Dlaczego potrzebny jest prawdziwy przegląd, a nie tylko dobre nawyki

Najbardziej frustrujące w SQL injection jest to, że kod może wyglądać na całkowicie czysty w 999 miejscach i mieć jedno surowe zapytanie, w jednym rzadko używanym narzędziu administracyjnym, które niweczy całą resztę. Automatyczne skanery wyłapują część takich przypadków, ale to rzetelny, staranny ręczny przegląd tego, jak faktycznie budowane są zapytania – a nie tylko sprawdzenie, czy używany jest ORM – pozwala znaleźć te, które naprawdę mają znaczenie.

Taki ręczny przegląd bezpieczeństwa aplikacji – rzeczywista analiza tego, jak działają zapytania i logika kontroli dostępu, a nie samo uruchomienie skanera i odhaczenie zadania – to trzon pracy Burncode w obszarze bezpieczeństwa, obok utwardzania infrastruktury i przygotowywania zespołów do prawdziwego audytu zgodności.