SQL injection: jak działa i jak go powstrzymać
SQL injection po dekadach wciąż należy do najczęściej wykorzystywanych podatności. Zobacz, jak atak wyciąga rekordy z bazy danych, i poznaj zabezpieczenia, które go powstrzymują.
Spis treści
Dlaczego SQL injection wciąż istnieje
SQL injection po raz pierwszy opisano w 1998 roku. Pojawia się na każdej liście OWASP Top 10 od początku jej istnienia. Odpowiada za jedne z największych wycieków danych w historii — TalkTalk (4 mln rekordów), Heartland Payment Systems (130 mln numerów kart) i niezliczone inne.
Nie przetrwał z powodu złożoności. SQL injection jest prosty do zrozumienia i, co kluczowe, prosty do zapobieżenia. Przetrwał, bo programiści sklejają dane od użytkownika z zapytaniami SQL, nie myśląc o konsekwencjach — często pod presją czasu i bez szkoleń z bezpieczeństwa.
Ten poradnik pokazuje dokładnie, jak działa, i konkretne techniki, które go eliminują.
Jak normalnie działa zapytanie SQL
Weźmy formularz logowania. Aplikacja pobiera nazwę użytkownika i hasło i sprawdza je w bazie danych:
SELECT * FROM users WHERE username = 'alice' AND password = 'secretpass';
Jeśli zapytanie zwróci wiersz, użytkownik jest uwierzytelniony. To normalne, oczekiwane zachowanie.
Podatność pojawia się, gdy programista buduje to zapytanie, sklejając ciągi znaków bezpośrednio z danych wpisanych przez użytkownika:
# Vulnerable Python code
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "';"
Zakłada się, że użytkownicy podadzą normalne wartości. Atakujący tego nie robią.
Atak: wyjście poza ciąg znaków
SQL używa apostrofów do oznaczania wartości tekstowych. Jeśli atakujący poda nazwę użytkownika zawierającą apostrof, może wyjść poza zamierzony ciąg znaków i wstrzyknąć własny kod SQL.
Dane wpisane przez atakującego:
- Nazwa użytkownika:
' OR '1'='1 - Hasło:
anything
Powstałe zapytanie:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything';
Ponieważ OR '1'='1' jest zawsze prawdziwe, zapytanie zwraca wszystkich użytkowników. Logowanie udaje się na konto pierwszego użytkownika w bazie — zwykle administratora.
Całkowite pominięcie sprawdzania hasła:
- Nazwa użytkownika:
admin'-- --to komentarz w SQL; wszystko po nim jest ignorowane
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything';
-- Becomes:
SELECT * FROM users WHERE username = 'admin';
Uwierzytelnianie zostaje ominięte. Pole hasła nigdy nie jest sprawdzane.
Nie tylko logowanie: wyciąganie danych
SQL injection nie ogranicza się do obejścia uwierzytelniania. Poważniejsze ataki wyciągają dane z całej bazy.
Wstrzykiwanie z UNION
Operator SQL UNION łączy wyniki dwóch zapytań SELECT. Atakujący może go użyć do pobrania danych z innych tabel:
-- Original query
SELECT name, description FROM products WHERE id = 1;
-- Injected
SELECT name, description FROM products WHERE id = 1
UNION SELECT username, password FROM users--;
Do wyników produktów zwracanych atakującemu zostają dołączone wszystkie nazwy użytkowników i hasła.
Blind SQL injection (wstrzykiwanie na ślepo)
Wiele aplikacji nie wyświetla użytkownikom błędów bazy ani wyników zapytań. Blind SQL injection wyciąga dane bit po bicie, zadając pytania typu prawda/fałsz:
-- Is the first character of the admin password 'a'?
SELECT * FROM users WHERE id = 1 AND SUBSTRING(password, 1, 1) = 'a';
Zapytanie sprawdza, czy pierwszy znak hasła administratora to „a”. Jeśli strona zachowuje się inaczej, gdy warunek jest prawdziwy, a inaczej, gdy fałszywy, atakujący może wywnioskować odpowiedź. Zautomatyzowane narzędzia, takie jak sqlmap, potrafią w ten sposób wyciągnąć całą bazę w kilka minut.
Kanały pozapasmowe i zapytania łączone
W niektórych systemach bazodanowych atakujący mogą:
- Wykonywać wiele poleceń naraz (stacked queries): usuwać tabele, tworzyć konta-furtki
- Odczytywać pliki z systemu plików serwera (
LOAD_FILE()w MySQL) - Zapisywać pliki na serwerze (potencjalnie tworząc web shell)
- Wywoływać połączenia sieciowe, by wyprowadzić dane kanałem pozapasmowym
W najgorszym przypadku SQL injection oznacza całkowite przejęcie serwera, a nie tylko ujawnienie danych.
Zabezpieczenia, które naprawdę działają
Zabezpieczenie 1: zapytania parametryzowane (prepared statements)
To podstawowa ochrona, która eliminuje SQL injection, a nie tylko ją ogranicza.
W zapytaniu parametryzowanym kod SQL i dane są wysyłane do bazy osobno. Baza najpierw kompiluje szablon SQL, a potem podstawia wartości od użytkownika. Wartości nigdy nie mogą zostać zinterpretowane jako SQL — zawsze są traktowane jako dane.
Python z zapytaniem parametryzowanym:
# Safe
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
Java z PreparedStatement:
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
Bez względu na to, co użytkownik wpisze w pole username — łącznie z ' OR '1'='1 — zostanie to potraktowane jako dosłowna wartość tekstowa, a nie SQL. Wstrzyknięcie jest niemożliwe.
Zabezpieczenie 2: ORM i konstruktory zapytań
Mapery obiektowo-relacyjne (ORM), takie jak SQLAlchemy, Hibernate, ActiveRecord czy Django ORM, wewnętrznie używają zapytań parametryzowanych. Prawidłowe korzystanie z ORM zapobiega SQL injection niemal we wszystkich przypadkach:
# Django ORM — safe
User.objects.filter(username=username, password=password)
Uwaga: większość ORM-ów ma furtki do surowego SQL (raw(), execute()), które ponownie wprowadzają ryzyko, jeśli dane użytkownika są w nich sklejane. Traktuj metody surowego SQL z taką samą ostrożnością jak ręczne budowanie zapytań.
Zabezpieczenie 3: procedury składowane
Procedury składowane mogą być bezpieczne, jeśli są poprawnie zaimplementowane — muszą wewnętrznie używać parametryzowanych danych wejściowych. Procedura, która wewnętrznie skleja ciągi znaków, wciąż jest podatna.
Zabezpieczenie 4: zasada najmniejszych uprawnień
Konto bazodanowe używane przez aplikację webową powinno mieć tylko niezbędne uprawnienia:
- Aplikacja głównie odczytująca dane potrzebuje tylko
SELECT - Żadna aplikacja nie powinna działać jako
root,saczyDBA - Różne komponenty aplikacji mogą korzystać z różnych kont bazodanowych z różnymi uprawnieniami
To ogranicza skutki udanego wstrzyknięcia: atakujący, który może tylko wykonywać SELECT, nie usunie tabel ani nie zapisze plików.
Zabezpieczenie 5: walidacja danych wejściowych
Sprawdzaj, czy dane wejściowe mają oczekiwany format:
- Identyfikator użytkownika powinien być dodatnią liczbą całkowitą — odrzuć wszystko inne, zanim trafi do zapytania
- Nazwa produktu nie powinna zawierać słów kluczowych SQL — stosuj listę dozwolonych znaków
Walidacja danych wejściowych to środek obrony w głąb, a nie podstawowe zabezpieczenie. Zmniejsza powierzchnię ataku, ale nie powinna być jedyną ochroną.
Zabezpieczenie 6: WAF (zapora aplikacji webowych)
WAF może wykrywać i blokować typowe wzorce SQL injection. To dodatkowa warstwa, a nie zamiennik zapytań parametryzowanych. WAF-y da się obejść sztuczkami z zaciemnianiem i kodowaniem. Nie polegaj na nich jako jedynej ochronie.
Testowanie pod kątem SQL injection
Podstawy testów ręcznych:
- Wpisz pojedynczy apostrof (
') w każde pole — komunikaty błędów bazy często ujawniają miejsca podatne na wstrzyknięcie - W parametrach liczbowych spróbuj
1=1i1=2i porównaj odpowiedzi - Spróbuj
'; SELECT SLEEP(5);--— jeśli odpowiedź się opóźni, występuje blind injection
Narzędzia automatyczne:
- sqlmap — standardowe narzędzie do wykrywania i wykorzystywania podatności (tylko w systemach, które masz prawo testować)
- Burp Suite — przechwytywanie i modyfikowanie żądań; w wersji Pro ma skaner SQL injection
- OWASP ZAP — darmowy skaner z wykrywaniem SQL injection
Lista kontrolna naprawy
- Zastąp całe sklejanie ciągów w zapytaniach do bazy zapytaniami parametryzowanymi
- Przejrzyj wszystkie wywołania surowego SQL w kodzie korzystającym z ORM
- Zastosuj konta bazodanowe z najmniejszymi uprawnieniami
- Wyłącz na produkcji wyświetlanie błędów bazy (błędy ujawniają informacje o schemacie)
- Przejrzyj i przetestuj procedury składowane
- Dodaj reguły WAF jako dodatkową warstwę
- Przed każdym wydaniem uruchom sqlmap lub odpowiednik na środowisku testowym
SQL injection ma kompletne, dobrze znane rozwiązanie. Nie ma powodu, by pojawiał się w nowym kodzie. Jedyną barierą jest świadomość i dyscyplina podczas programowania.