Każde zapytanie SQL zbudowane przez sklejanie stringów z niezaufanymi wartościami może zostać wstrzyknięte, a poprawka jest taka sama w każdym języku: zostaw SQL w stałej postaci, a wartości przekazuj jako parametry. OWASP Query Parameterization Cheat Sheet zalicza SQL injection do kategorii A05:2025-Injection w OWASP Top 10:2025 i nazywa ją jedną z najgroźniejszych podatności aplikacji webowych.
Dlaczego sklejanie zawodzi
SQL nie rozróżnia naprawdę płaszczyzny sterowania i płaszczyzny danych. Atakujący umieszcza w danych wejściowych metaznak i w zapytaniu pojawiają się polecenia SQL, których wcześniej tam nie było. Strona społeczności OWASP o SQL injection pokazuje zapytanie w C# zbudowane przez konkatenację:
string query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ ItemName.Text + "'";
Jeśli atakujący wpisze jako nazwę przedmiotu name' OR 'a'='a, warunek zawsze będzie prawdziwy. Zapytanie zwróci wszystkie wiersze tabeli items, niezależnie od właściciela. W zależności od funkcji bazy i uprawnień ta sama luka może też pozwolić na modyfikację danych, wpływ na pliki albo uruchamianie poleceń systemu operacyjnego.
Pochodzenie wartości nie ma znaczenia. Zabezpieczenia obowiązują niezależnie od tego, czy dane przychodzą z żądania HTTP, z innej usługi, czy z wcześniej zapisanych danych.
Znajdź zapytania budowane ze stringów
Zacznij od grepa po słowach kluczowych SQL występujących w pobliżu konkatenacji lub formatowania. Te wzorce to zgrubne pierwsze przejście, a nie skaner. Generują sporo szumu i pomijają zapytania rozbite na kilka linii.
# concatenation (Java, C#, JavaScript, PHP uses "." instead of "+")
grep -rnEi "(select|insert into|update|delete from)[^;]*[\"'] *\+" src/
# Python f-strings and .format()
grep -rnEi "f[\"'](select|insert|update|delete)|(select|insert|update|delete).*\.format\(" .
# JavaScript template literals
grep -rnEi '`(select|insert|update|delete)[^`]*\$\{' src/
# sprintf-style formatting
grep -rnEi "sprintf\(.*(select|insert|update|delete)" .
Przeczytaj każdy wynik i sprawdź, czy niezaufane wartości są przekazywane jako parametry, a nie doklejane. Następnie zajrzyj w miejsca, które grep zwykle pomija:
- Zapytania w ORM zapisane jako stringi. Zapytania Hibernate mogą być podatne, jeśli wartości są doklejane do stringa zapytania zamiast być przekazywane jako parametry.
- Procedury składowane. Trzymają SQL w bazie, ale nie są automatycznie bezpieczne. OWASP pokazuje procedurę PL/SQL, która skleja
usriitmw string zapytania, i stwierdza, że jest ona „podatna na ten sam atak SQL injection, który pokazano w pierwszym przykładzie". Parametryzacja argumentów wywołania chroni samo wywołanie, ale nie dynamiczny SQL budowany wewnątrz procedury. - Parametryzacja po stronie klienta. Wiele bibliotek klienckich buduje zapytania przez konkatenację stringów i wysyła na serwer surowe zapytanie. Upewnij się, że parametryzacja odbywa się po stronie serwera.
- Dynamiczne identyfikatory i kierunki sortowania. Nie da się ich przekazać jako parametrów, więc to właśnie tam konkatenacja przetrwa przegląd parametryzacji.
Potem przetestuj. OWASP Web Security Testing Guide zawiera testy SQL injection do testów ręcznych i automatycznych, w tym techniki blind i out-of-band. Zewnętrzną weryfikację tego, co umknęło w Twoim przeglądzie, zapewni test penetracyjny.
Zastąp je parametrami
Zdefiniuj strukturę SQL oddzielnie od wartości danych, a następnie przekaż wartości przez API bazy danych. To przykład z OWASP SQL Injection Prevention Cheat Sheet, z pominięciem zwalniania zasobów i obsługi błędów:
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, customerName);
ResultSet results = statement.executeQuery();
Wartość taka jak tom' or '1'='1 jest teraz traktowana jako dosłowna nazwa użytkownika do dopasowania. Doklejenie niezaufanej wartości do zapytania przed jego przygotowaniem (prepare) niweczy tę ochronę.
W ORM również przekazuj wartości jako parametry w stringu zapytania. Przykłady OWASP używają nazwanego parametru w HQL, FROM Inventory i WHERE i.productDescription = :productDescription, a wartość przekazują przez setParameter.
W dynamicznym SQL wewnątrz procedur składowanych używaj zmiennych wiązanych (bind variables). W Oracle PL/SQL oznacza to EXECUTE IMMEDIATE z USING. W SQL Server - sp_executesql z zadeklarowaną listą parametrów. Procedury bez dynamicznego SQL nie wymagają niczego specjalnego, bo ich parametry są już związane.
Parametryzacja uniemożliwia wartości zmianę składni SQL. Nie rozstrzyga jednak, czy wywołujący może mieć dostęp do danego konta, więc kontrole autoryzacji muszą pozostać na miejscu.
Czego parametry nie obejmują
Znaczniki parametrów zastępują wartości danych. Nie mogą zastępować nazw tabel, nazw kolumn ani słów kluczowych, takich jak ASC i DESC. Gdy użytkownik musi dokonać wyboru, zmapuj jego decyzję na niewielki zestaw tokenów należących do aplikacji i odrzucaj wszystko, co nieznane. Przed:
String query = "SELECT id, name FROM items ORDER BY name " + sortParam;
Po:
Map<String, String> directions = Map.of("asc", "ASC", "desc", "DESC");
String direction = directions.get(sortParam);
if (direction == null) {
throw new IllegalArgumentException("Unknown sort direction");
}
String query = "SELECT id, name FROM items ORDER BY name " + direction;
Tekst doklejany do zapytania jest teraz zawsze jednym z dwóch literałów, nigdy tym, co wysłał użytkownik. Nadal przekazuj jako parametry każdą wartość danych w tym samym zapytaniu, nawet jeśli przechodzi kontrolę typu, długości lub formatu.
Nie wracaj do ręcznego escapowania. Zależy ono od dialektu SQL i konfiguracji: na przykład tryb NO_BACKSLASH_ESCAPES w MySQL wyłącza escapowanie ukośnikiem odwrotnym. OWASP określa escapowanie jako zdecydowanie niezalecane, a listy zabronionych wartości (deny lists) opisuje jako pełne luk.
Ogranicz skutki
Zasada najmniejszych uprawnień nie zapobiega SQL injection. Ogranicza to, co atakujący może zrobić przez konto bazodanowe, więc stosuj ją obok parametrów.
- Zacznij od zera i dodawaj uprawnienia potrzebne każdemu kontu aplikacji. Ustalanie, co odebrać z szerokiego nadania uprawnień, to sposób na przeoczenie uprawnień.
- Dostęp DBA lub administratora dla konta aplikacji to typowy skrót, bo wszystko „po prostu działa". OWASP nazywa to bardzo niebezpiecznym.
- Strona logowania potrzebuje dostępu do odczytu pól z nazwą użytkownika i hasłem, bez prawa zapisu. Strona rejestracji potrzebuje insert. Taki podział da się wymusić tylko wtedy, gdy obie używają różnych użytkowników bazy danych, więc daj każdej aplikacji webowej własnego.
- Atakujący nie potrzebują injection, by nadużyć konta z nadmiernymi uprawnieniami. Mogą zmienić parametr z dozwolonej wartości na taką, do której sami nie mają uprawnień, ale aplikacja ma, dlatego wąskie uprawnienia pomagają nawet tam, gdzie zapytania są poprawne.
- Widok może udostępniać tylko pola potrzebne kontu, przy czym
SELECTnadajesz na widok, a nie na tabelę. Najpierw sprawdź reguły swojej bazy: PostgreSQL zwykle sprawdza dostęp do tabel źródłowych z uprawnieniami właściciela widoku. Zapytania do widoków nadal wymagają parametrów. - Ważne jest też konto systemu operacyjnego. Nie uruchamiaj DBMS jako root ani system. OWASP zwraca uwagę, że MySQL domyślnie działa w Windows jako system.
Od czego zacząć
- Wypisz każde miejsce, które buduje SQL: kod aplikacji, zapytania ORM zapisane jako stringi i procedury składowane używające dynamicznego SQL. Uruchom powyższe wzorce grep jako pierwsze przejście.
- Zastąp doklejane wartości parametrami po stronie serwera, a dynamiczny SQL wewnątrz procedur przebuduj na zmienne wiązane.
- Zastąp dynamiczny wybór tabel, kolumn i sortowania stałymi mapowaniami, które odrzucają nieznane dane wejściowe.
- Zrób audyt kont bazodanowych. Odbierz uprawnienia DBA i administratora kontom aplikacji, daj każdej aplikacji własnego użytkownika, a dostęp tylko do odczytu przenieś na wąskie uprawnienia lub widoki.
- Uruchom testy SQL injection z Web Security Testing Guide na efekcie zmian albo poproś nasz zespół bezpiecznego wytwarzania oprogramowania o przegląd poprawek.
