SQL injection to błąd w budowaniu tekstu
SQL injection zdarza się, gdy dane od użytkownika stają się częścią tekstu SQL, który parsuje baza. Gdy ta granica się zaciera, czyli gdy wartość wpisana przez użytkownika staje się składnią wykonywaną przez bazę, użytkownik może zrobić wszystko to, co ty.
Oto klasyczny antywzorzec w pseudokodzie, który da się napisać w każdym języku:
-- NIE RÓB TEGO
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
Jeśli user_input to Ada, dostajesz zwykłe wyszukiwanie. Jeśli user_input to ' OR 1=1 --, dostajesz:
SELECT * FROM users WHERE name = '' OR 1=1 --'
-- zamienia końcowy cudzysłów w komentarz, OR 1=1 pasuje do każdego wiersza, a atakujący właśnie wyciągnął całą tabelę użytkowników. Gorsze wersje doklejają ; i drugą instrukcję, by usuwać tabele, wynosić dane albo dodać nowe konto administratora.
Podatność nie leży w SQLite. Leży w kodzie, który zbudował ten tekst.
Zapytania parametryzowane: prawdziwe rozwiązanie
Zapytanie parametryzowane oddziela tekst SQL od wartości. SQL ma symbole zastępcze, ? lub :name, a wartości przekazujesz obok. SQLite parsuje i kompiluje SQL raz, a potem wiąże twoje wartości ze skompilowanym planem. Wartości nie mogą stać się SQL.
Uruchom wyszukiwanie, które wygląda na podatne, w bezpieczny sposób:
W powłoce SQLite dosłownie wpisujesz wartość, ale w kodzie aplikacji odpowiednik wygląda tak (sterownik sqlite3 w Pythonie):
# Python: parametryzowane, bezpieczne
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
Przekaż SQL i krotkę wartości jako dwa osobne argumenty. Sterownik wysyła je do SQLite osobno. Nawet jeśli user_input to ' OR 1=1 --, SQLite szuka użytkownika, który dosłownie nazywa się ' OR 1=1 --, i nikogo nie znajduje.
Co tu właściwie znaczy "bezpieczne"
Bezpieczeństwo nie wynika z dopasowywania wzorców ani escapowania. Wynika ze struktury. SQLite kompiluje instrukcję do wewnętrznej postaci, zanim w ogóle zobaczy twoją wartość:
-- Skompilowana instrukcja ma miejsce na wartość, a nie tekst.
SELECT * FROM users WHERE name = ?
^
miejsce na parametr
Gdy wiążesz wartość, trafia ona w to miejsce jako dana z typem: TEXT, INTEGER, BLOB czy inna. SQLite nigdy nie parsuje jej ponownie jako SQL. Nie ma składni, którą atakujący mógłby wstrzyknąć, bo parser już skończył swoją pracę.
Dlatego zapytania parametryzowane są niezawodne w sposób, w jaki escapowanie nigdy nie będzie. Escapowanie próbuje wyczyścić niebezpieczne znaki z tekstu. Wiązanie w ogóle nie buduje niebezpiecznego tekstu.
Nie sięgaj po formatowanie tekstu
Każdy język ma kuszący skrót: f-stringi w Pythonie, literały szablonowe w JavaScript, String.format w Javie. Każdy z nich to strzał w stopę przy SQL.
# NIE: f-string wstawia wartość do tekstu SQL
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# NIE: ten sam problem, formatowanie przez %
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)
# TAK: symbol zastępczy + argument z wartościami
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
Pierwsze dwa wstawiają dane od użytkownika do tekstu SQL, zanim sterownik w ogóle go zobaczy. Gdy zapytanie dociera do SQLite, szkoda już się stała. Trzeci trzyma SQL i wartość na osobnych torach.
Ta zasada jest mechaniczna: jeśli kiedykolwiek łapiesz się na budowaniu tekstu SQL przez +, f-stringi, format albo literały szablonowe w miejscu, gdzie idzie wartość, zatrzymaj się i użyj symbolu zastępczego.
Wiele parametrów i nazwane symbole zastępcze
Prawdziwe zapytania zwykle mają więcej niż jedną wartość. SQLite obsługuje zarówno pozycyjne ?, jak i nazwane :name:
W kodzie aplikacji wygląda to tak:
# Pozycyjne
cursor.execute(
"SELECT * FROM orders WHERE customer = ? AND status = ?",
("Ada", "paid"),
)
# Nazwane: czytelniejsze, gdy parametrów jest kilka
cursor.execute(
"SELECT * FROM orders WHERE total > :min_total AND status = :status",
{"min_total": 50, "status": "paid"},
)
Nazwane parametry lepiej się skalują. Przy więcej niż trzech czy czterech wartościach ?, ?, ?, ? zamienia się w zgadywankę, a :customer, :total, :status, :created_at samo się dokumentuje.
Identyfikatory wymagają innego podejścia
Związane parametry działają tylko dla wartości: tego, co stoi po prawej stronie =, wewnątrz IN (...), w VALUES (...). Nie działają dla nazw tabel, nazw kolumn ani słów kluczowych SQL takich jak ASC/DESC.
-- To NIE działa. Symbol zastępczy nie może zastąpić nazwy kolumny.
SELECT * FROM users ORDER BY ? ASC
Jeśli potrzebujesz dynamicznego identyfikatora, na przykład gdy użytkownik wybiera, po której kolumnie sortować, sprawdź go z listą dozwolonych wartości, zanim zbudujesz SQL:
# Podejście z listą dozwolonych
ALLOWED_SORT_COLUMNS = {"name", "created_at", "role"}
if sort_column not in ALLOWED_SORT_COLUMNS:
raise ValueError(f"Nieprawidłowa kolumna sortowania: {sort_column}")
query = f"SELECT * FROM users ORDER BY {sort_column} ASC"
cursor.execute(query)
Tekst od użytkownika jest sprawdzany ze stałym zestawem znanych, bezpiecznych wartości zanim zbliży się do SQL. F-string jest tu dopuszczalny tylko dlatego, że sort_column nie może już być niczym innym niż jedną z trzech wpisanych na sztywno nazw.
Konkretna próba ataku, rozbrojona
Zobaczmy obie wersje obok siebie z wrogim wejściem. Przygotuj małą tabelę użytkowników:
Podatna forma zwraca wszystkich użytkowników. Forma parametryzowana szuka użytkownika, który dosłownie nazywa się ' OR 1=1 --, i nic nie zwraca. To samo wejście, zupełnie inny wynik, bo w drugim przypadku wartość nigdy nie stała się SQL.
Krótka lista kontrolna
- Używaj symboli zastępczych
?lub:namedla każdej wartości spoza twojego kodu: danych od użytkownika, treści żądań, zmiennych środowiskowych, wszystkiego, czego nie wpisano bezpośrednio w kodzie. - Nigdy nie buduj SQL przez
+, f-stringi aniformatw miejscu, gdzie idzie wartość. - Dynamiczne nazwy tabel i kolumn sprawdzaj ze stałą listą dozwolonych, zanim wstawisz je do zapytania.
- Zaufaj sterownikowi. Nie pisz własnej funkcji do escapowania cudzysłowów. Mechanizm związanych parametrów jest starszy, lepiej sprawdzony w boju i poprawny.
- Przeglądaj zapytania zespołu z jednym pytaniem: czy jakiekolwiek dane od użytkownika są doklejane do tekstu SQL? Jeśli tak, popraw to.
Wyrób sobie ten nawyk, a SQL injection przestanie być klasą błędów, o której musisz myśleć.
Dalej: łączenie się z aplikacji
Znasz już bezpieczny kształt zapytania: symbol zastępczy w SQL, wartość przekazywana obok. Następna strona pokazuje, jak faktycznie podłączyć SQLite w prawdziwym kodzie aplikacji w Pythonie, Node.js i kilku innych językach, łącznie z zarządzaniem połączeniami i miejscem zapytań parametryzowanych w typowym przepływie żądania.
Najczęściej zadawane pytania
Czy SQLite jest podatny na SQL injection?
Tak. SQLite jest tak samo podatny jak każda inna baza SQL, gdy kod aplikacji buduje zapytania przez sklejanie tekstów. Rozwiązaniem nie jest żadne ustawienie SQLite, tylko sposób przekazywania wartości z aplikacji. Używaj zapytań parametryzowanych z symbolami zastępczymi ? lub :name, a sterownik obsłuży to bezpiecznie.
Jak zapytania parametryzowane chronią przed SQL injection?
Gdy używasz symboli zastępczych takich jak ?, SQLite najpierw parsuje i kompiluje zapytanie, a dopiero potem wiąże twoje wartości z miejscami w już skompilowanej instrukcji. Wartości nigdy nie mogą stać się składnią SQL: są traktowane jako dane i koniec. Nie ma tekstu, z którego atakujący mógłby się wyrwać.
Czy zamiast tego mogę po prostu escapować cudzysłowy w danych od użytkownika?
Nie. Ręczne escapowanie jest kruche: przeoczysz jakiś przypadek brzegowy (cudzysłowy Unicode, sztuczki z kodowaniem, znaczniki komentarzy) i wypuścisz podatność. Sterowniki udostępniają parametry ? i :name właśnie po to, żeby nie trzeba było myśleć o escapowaniu. Używaj ich za każdym razem, nawet dla wartości, o których 'wiesz', że są bezpieczne.
A co z nazwami tabel lub kolumn, które pochodzą od użytkownika?
Związane parametry działają tylko dla wartości, nie dla identyfikatorów. Jeśli nazwa tabeli lub kolumny musi być dynamiczna, sprawdź ją z listą dozwolonych nazw, zanim wstawisz ją do SQL. Nigdy nie przepuszczaj surowego identyfikatora od użytkownika przez formatowanie tekstu.