Formularz logowania działa, wyszukiwarka produktów przyjmuje nietypowy znak, a aplikacja nagle pokazuje błąd bazy danych. W takich sytuacjach problemem może być SQL injection, czyli podatność pozwalająca potraktować dane użytkownika jak fragment polecenia SQL. Wyjaśniam, jak działa ten atak, jakie szkody powoduje oraz jak zabezpieczyć aplikację, także pisaną w Pythonie.
Najważniejsza zasada polega na oddzieleniu danych od kodu zapytania
- SQL injection wykorzystuje sytuację, w której dane wejściowe zostają połączone z tekstem zapytania SQL.
- Atak może prowadzić do odczytu, zmiany lub usunięcia danych, a czasem także do dalszego przejęcia systemu.
- Najskuteczniejszą ochroną są zapytania parametryzowane, a nie ręczne filtrowanie znaków.
- W Pythonie parametry przekazuje się osobno, na przykład przez placeholdery biblioteki sqlite3 lub sterownika PostgreSQL.
- Dodatkową warstwę bezpieczeństwa zapewniają minimalne uprawnienia, bezpieczne komunikaty błędów i testy.

SQL injection w praktyce oznacza pomieszanie danych z kodem
SQL injection to atak na aplikację, która komunikuje się z relacyjną bazą danych, na przykład MySQL, PostgreSQL, Microsoft SQL Server albo SQLite. Podatność pojawia się wtedy, gdy aplikacja buduje zapytanie przez doklejanie danych użytkownika do tekstu SQL.
Wyobraźmy sobie prostą funkcję logowania. Program pobiera login i hasło, a potem tworzy zapytanie w rodzaju:
SELECT id FROM users
WHERE username = 'wpisany_login'
AND password = 'wpisane_haslo';
Samo zapytanie nie jest problemem. Ryzyko pojawia się wtedy, gdy zawartość pól zostaje wstawiona do niego bez odpowiedniego rozdzielenia. Wtedy specjalnie przygotowany tekst może zmienić znaczenie całego polecenia, zamiast pozostać zwykłą wartością wyszukiwaną w bazie.
Najprościej porównać to do formularza, w którym użytkownik powinien podać adres, ale aplikacja pozwala mu dopisać instrukcję dla kuriera. Baza danych nie rozumie intencji programisty. Wykonuje otrzymane polecenie zgodnie ze składnią i uprawnieniami konta, z którego korzysta aplikacja.
Jak powstaje podatność w kodzie aplikacji
Najczęstszy błąd wygląda niewinnie. Programista łączy stały fragment zapytania z wartością pobraną z formularza, parametru URL albo nagłówka HTTP:
user_id = request.args.get("id")
query = "SELECT * FROM users WHERE id = " + user_id
cursor.execute(query)
Problem nie znika tylko dlatego, że aplikacja oczekuje liczby. Jeśli wartość nie jest bezpiecznie obsłużona, napastnik może próbować wpłynąć na strukturę zapytania. Walidacja typu pomaga, ale sama nie zastępuje parametryzacji, zwłaszcza gdy dane mają postać tekstu.
Niebezpieczne są także zapytania dynamiczne
Ryzyko dotyczy nie tylko logowania. Podatność może pojawić się w wyszukiwaniu produktów, filtrowaniu zamówień, raportach, sortowaniu i panelach administracyjnych. Szczególnie ostrożnie traktuję kod, który składa zapytania z wielu fragmentów tekstu zależnych od użytkownika.
Popularny ORM, czyli narzędzie mapujące obiekty programu na tabele, nie daje automatycznej gwarancji bezpieczeństwa. Bezpieczne są zwykle jego standardowe metody filtrowania, ale użycie surowego SQL z konkatenacją może ponownie otworzyć tę samą lukę. ORM nie zastępuje zasad bezpiecznego programowania.
Czego nie należy mylić z ochroną
- Usuwanie pojedynczych znaków, takich jak apostrof, nie jest pełną ochroną.
- Ukrywanie komunikatu błędu nie naprawia podatności, tylko utrudnia jej zauważenie.
- Filtrowanie słów typu
SELECTlubUNIONmożna ominąć i nie powinno być główną metodą zabezpieczenia. - Szyfrowanie bazy danych nie zapobiega wstrzyknięciu, jeśli aplikacja po odszyfrowaniu wykonuje niebezpieczne zapytanie.
Najczęściej widzę tu błędne założenie, że „zwykły formularz” nie jest atrakcyjny dla atakującego. Tymczasem automatyczne skanery sprawdzają tysiące aplikacji, a podatność może zostać wykryta bez ręcznego analizowania kodu.
Co może osiągnąć napastnik
Skutki zależą od konstrukcji zapytania, rodzaju bazy, uprawnień konta i dodatkowych zabezpieczeń. W łagodnym scenariuszu aplikacja zwróci błąd lub nieprawidłowy wynik. W gorszym przypadku atakujący uzyska dostęp do danych, których nie powinien widzieć.
| Wariant ataku | Możliwy skutek | Dlaczego jest groźny |
|---|---|---|
| Odczyt danych | Pobranie rekordów użytkowników, zamówień lub konfiguracji | Może naruszyć poufność i ujawnić dane osobowe |
| Modyfikacja danych | Zmiana adresów, cen, uprawnień albo statusów | Wpływa na działanie biznesowe aplikacji |
| Usunięcie danych | Skasowanie rekordów lub części tabel | Może spowodować przestój i utratę informacji |
| Ominięcie autoryzacji | Dostęp do konta lub zasobów bez prawidłowego uwierzytelnienia | Atakujący może wejść w rolę innego użytkownika |
| Rozpoznanie systemu | Ustalenie wersji bazy, tabel lub sposobu działania aplikacji | Ułatwia przygotowanie kolejnych etapów ataku |
Wyróżnia się między innymi ataki error-based, które wykorzystują komunikaty błędów, oraz blind SQL injection, gdzie aplikacja nie pokazuje danych bezpośrednio, ale zdradza informacje przez różnice w odpowiedziach albo czasie reakcji. Istnieją też techniki wykorzystujące łączenie wyników zapytań, lecz ich szczegóły powinny być testowane wyłącznie w autoryzowanym środowisku.
Największe szkody nie zawsze wynikają z samej bazy. Jeśli konto aplikacji ma zbyt szerokie uprawnienia, luka może zostać użyta do odczytu tabel administracyjnych, danych klientów czy sekretów zapisanych w bazie. Dlatego zasada najmniejszych uprawnień ogranicza zasięg incydentu, choć nie usuwa źródła problemu.
Jak zabezpieczyć aplikację Python
Podstawowa reguła jest prosta: kod zapytania definiujemy osobno, a wartości przekazujemy jako parametry. Sterownik bazy sam zajmuje się ich właściwym przekazaniem, dzięki czemu tekst użytkownika nie staje się instrukcją SQL.
Niebezpieczny wariant:
email = request.form["email"]
query = "SELECT id FROM users WHERE email = '" + email + "'"
cursor.execute(query)
Bezpieczniejszy wariant dla SQLite:
email = request.form["email"]
query = "SELECT id FROM users WHERE email = ?"
cursor.execute(query, (email,))
W sterownikach PostgreSQL składnia placeholdera może wyglądać inaczej, na przykład:
query = "SELECT id FROM users WHERE email = %s"
cursor.execute(query, (email,))
Nie należy kopiować znaku zapytania między bibliotekami bez sprawdzenia dokumentacji sterownika. Sama idea pozostaje jednak taka sama: parametr nie jest sklejany z zapytaniem, tylko przekazywany osobnym argumentem.
Co zrobić z nazwą kolumny lub sortowaniem
Parametryzacja dobrze obsługuje wartości, ale zwykle nie pozwala bezpośrednio podstawić nazwy tabeli, kolumny czy kierunku sortowania. W takim przypadku używam listy dozwolonych wartości, a nie danych wklejanych bezpośrednio do SQL:
allowed_sort = {
"name": "name",
"price": "price",
"newest": "created_at"
}
sort_key = request.args.get("sort", "newest")
sort_column = allowed_sort.get(sort_key, "created_at")
query = f"SELECT id, name, price FROM products
ORDER BY {sort_column}"
cursor.execute(query)
Użytkownik wybiera tu jeden z wcześniej zdefiniowanych kluczy, a aplikacja sama przypisuje mu bezpieczną nazwę kolumny. To dobre miejsce na allowlistę, czyli listę akceptowanych opcji. Nie należy próbować rozwiązywać tego przez ręczne usuwanie podejrzanych znaków.
Warstwy, które warto dołożyć
- Używaj konta bazy z uprawnieniami ograniczonymi do operacji potrzebnych aplikacji.
- Nie zapisuj haseł użytkowników w postaci jawnego tekstu. Przechowuj ich bezpieczne skróty z użyciem odpowiedniego mechanizmu do haseł.
- Pokazuj użytkownikowi ogólny komunikat błędu, a szczegóły zapisuj w kontrolowanych logach.
- Aktualizuj sterowniki, biblioteki i system bazy danych.
- Nie umieszczaj haseł do bazy w kodzie źródłowym ani w repozytorium.
- Testuj także ścieżki administracyjne, API i zadania wykonywane w tle.
Ręczne escapowanie znaków traktuję najwyżej jako rozwiązanie awaryjne dla bardzo konkretnego, starego interfejsu. Zapytania parametryzowane są prostsze do utrzymania i znacznie trudniej przypadkowo zepsuć je podczas późniejszej rozbudowy aplikacji.
Jak wykrywać SQL injection i reagować na problem
Wykrywanie powinno zacząć się od przeglądu kodu. Szukam miejsc, w których do metod wykonujących zapytania trafia tekst z formularza, adresu URL, ciasteczka lub nagłówka. Podejrzane są szczególnie operatory konkatenacji, f-stringi i formatowanie łańcuchów używane bezpośrednio przy budowaniu SQL.
Drugim krokiem są testy w środowisku testowym. Można sprawdzać, czy nietypowe znaki powodują błędy, różnice w odpowiedziach lub nieoczekiwane wyniki, ale nie wolno testować cudzych systemów bez zgody. Do kontrolowanych audytów służą między innymi narzędzia analizujące aplikacje webowe, skanery DAST oraz testy integracyjne.
Przeczytaj również: Plik CSV - Jak działa, dlaczego sprawia problemy i jak go używać?
Co sprawdzić po znalezieniu luki
- Ogranicz dostęp do podatnego endpointu, jeśli jest to możliwe bez większej awarii.
- Zidentyfikuj wszystkie miejsca korzystające z tego samego wzorca kodu.
- Zastąp konkatenację parametryzacją i dodaj test regresyjny.
- Sprawdź logi pod kątem nietypowych zapytań, błędów i dużej liczby odrzuconych żądań.
- Zweryfikuj, czy konto bazy nie ma nadmiarowych uprawnień.
- Oceń, czy mogło dojść do odczytu lub zmiany danych, i uruchom procedurę reagowania na incydenty.
Sam komunikat „błąd SQL” nie jest jeszcze dowodem udanego ataku. Jest jednak sygnałem, że aplikacja może ujawniać zbyt dużo informacji albo nieprawidłowo obsługuje dane wejściowe. Błąd widoczny dla użytkownika powinien prowadzić do analizy, a nie tylko do wyciszenia wyjątku.
Co sprawdzić przed wdrożeniem aplikacji do produkcji
Przed publikacją aplikacji robię krótki przegląd wszystkich miejsc, w których kod dotyka bazy. Najważniejsze pytanie brzmi nie „czy filtrujemy wejście?”, lecz „czy dane są wszędzie przekazywane jako parametry?”.
- Przeszukaj kod pod kątem
execute(), f-stringów i konkatenacji przy zapytaniach. - Sprawdź formularze, API, importy plików i zadania cykliczne.
- Dodaj testy dla pustych wartości, znaków specjalnych i nieprawidłowych typów.
- Zweryfikuj uprawnienia konta aplikacji oraz dostęp sieciowy do bazy.
- Wyłącz szczegółowe błędy SQL na produkcji.
- Skonfiguruj monitoring, kopie zapasowe i alerty dotyczące nietypowych zapytań.
SQL injection nie jest problemem, który rozwiązuje pojedynczy filtr albo instalacja dodatkowego modułu. Najlepsze efekty daje połączenie parametryzacji, ograniczonych uprawnień, testów i świadomego przeglądu kodu. W Pythonie podstawowa poprawka zwykle jest niewielka, ale trzeba zastosować ją konsekwentnie w całej aplikacji.
Jeżeli zapamiętasz jedną rzecz, niech będzie nią rozdzielenie kodu SQL od danych użytkownika. To prosta praktyka, która znacząco zmniejsza ryzyko, a przy okazji sprawia, że kod łatwiej testować, rozwijać i utrzymywać.
