Co to jest SQL injection i jak go zapobiegać?
Praktyczne wyjaśnienie ataków SQL injection, rzeczywiste przykłady i konkretne rozwiązania, które rzeczywiście je zatrzymują w kodzie produkcyjnym.
SQL injection istnieje od końca lat 90. i wciąż jest jednym z najczęstszych sposobów, na które aplikacje internetowe zostają zaatakowane. Błąd jest prosty: atakujący wysyła input, który zostaje zinterpretowany jako kod SQL zamiast zwykłych danych, a baza danych robi coś, czego nigdy nie powinna robić.
Jak atak faktycznie działa
Wyobraź sobie formularz logowania, który buduje zapytanie w taki sposób w PHP:
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
Jeśli aplikacja nie sanityzuje inputu, atakujący wpisuje admin' -- w pole username. Zapytanie staje się:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- komentuje resztę linii, więc sprawdzenie hasła nigdy się nie wykonuje. To jest klasyczne obejście autoryzacji. Atakujący też używają UNION SELECT aby pobierać dane z innych tabel, lub stackują zapytania średnikiem, aby uruchomić coś destrukcyjnego jak DROP TABLE users; jeśli driver na to pozwala.
Istnieje też blind SQL injection, gdzie aplikacja nie wyświetla wyników zapytania bezpośrednio. Atakujący wnioskują informacje przez timing (SLEEP(5) w MySQL) lub boolean responses (czy strona zachowuje się inaczej dla warunku true vs. false). Narzędzia takie jak sqlmap automatyzują tego rodzaju ekstrakcję, gdy znaleziona zostanie podatna zmienna.
Dlaczego konkatenacja stringów jest źródłem problemu
Każdy wariant tego ataku sprowadza się do jednego: mieszania kodu i danych w tym samym stringu. Baza danych nie potrafi rozróżnić między legytymną wartością a wstrzykniętą składnią, ponieważ przychodza przez ten sam kanał. Escapowanie cudzysłowów pomaga w niektórych przypadkach, ale jest kruche — różne kodowania, second-order injection (dane przechowywane raz, potem ponownie użyte niebezpiecznie później), i specyficzne dla drivera nieścisłości tworzą ścieżki bypassu. Escapowanie to łatka, nie naprawa.
Parameterized queries to rzeczywista naprawa
Naprawą jest oddzielenie kodu SQL od danych użytkownika na poziomie drivera, używając parameterized queries (zwanych też prepared statements). Baza danych najpierw otrzymuje strukturę zapytania, potem binduje wartości potem, więc input użytkownika nigdy nie zmienia znaczenia zapytania.
W Python z psycopg2:
cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
W Node.js z mysql2:
connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
W Java z JDBC:
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
Zauważ wzór: placeholdery (%s, ?) rezerwują miejsce dla danych, a rzeczywiste wartości są przekazywane osobno. Bez konkatenacji stringów, bez ręcznego escapowania. To działa dla ogromnej większości zapytań, które napiszesz w normalnej aplikacji.
Co z dynamicznymi nazwami tabel lub kolumn?
Parameteryzacja obsługuje wartości, ale nie identyfikatory — nie możesz bindować nazwy tabeli jako parametru. Jeśli aplikacja musi wybierać tabelę dynamicznie (rzadko, i często oznacza błąd designu), użyj whitelist dozwolonych wartości w stosunku do hardkodowanej listy zamiast ufać inputowi użytkownika bezpośrednio:
allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
raise ValueError("Invalid table")
Nigdy nie buduj nazw identyfikatorów przez string formatting z inputu użytkownika, nawet z escapowaniem.
Warstwowe obrony poza samym zapytaniem
Parameterized queries to główna kontrola, ale kilka innych rzeczy ma znaczenie:
- Least privilege na koncie bazy danych. Użytkownik DB aplikacji nie powinien mieć
DROP,ALTER, lub dostępu do niezwiązanych schematów. Jeśli injection przejdzie, ograniczone uprawnienia ograniczają szkodę. - ORMy pomagają domyślnie. Django ORM, SQLAlchemy i Hibernate automatycznie parameteryzują zapytania, gdy używasz ich standardowych metod query-buildingu. Ryzyko pojawia się, gdy developerzy spadają w raw SQL lub używają
.extra()/text()calls ze string interpolacją — więc audytuj konkretnie te miejsca. - Input validation to warstwa drugorzędna, nie zamiennik. Sprawdzanie, czy pole email wygląda na email to dobra praktyka, ale nie zatrzymuje injection samo w sobie — atakujący znajdują kreatywne payloady, które wciąż przechodzą luźną walidację.
- WAF może złapać znane wzorce ataków, ale to warstwa detekcji, nie naprawa bazowego kodu.
Testowanie własnego kodu na ten błąd
Puść swoje zapytania przez narzędzie static analysis (Bandit dla Python, Semgrep z SQL injection rulesets) jako część CI. Dla ręcznego testowania, spróbuj wstrzyknąć pojedynczy cudzysłów (') w każde pole inputu i obserwuj błędy SQL wyciękające w odpowiedzi — to często pierwszy znak, że zapytanie nie jest parameteryzowane.
Jeśli chcesz pójść głębiej w ten temat, Korra Studio Web Security track obejmuje injection obok XSS i auth bypass, a segmenty Databases przecodzą przez query design patterns, które całkowicie unikają tej klasy błędu.
Napisane z pomocą AI, zweryfikowane i opublikowane przez Michal Pilch (CISSP), Korra Studio.
To jedna notatka z bazy wiedzy Korra Studio — platforma łączy każdy temat z mentoringiem 1 na 1.
Zacznij za darmoarrow_forward