Zagrożenia bezpieczeństwa
Bazy danych często zawierają wrażliwe dane i pozwalają wykonywać niebezpieczne operacje. Dla bezpiecznej pracy z Nette Database kluczowe jest:
- Zrozumieć różnicę między bezpiecznym a niebezpiecznym API
- Używać zapytań parametryzowanych
- Poprawnie walidować dane wejściowe
Czym jest SQL injection?
SQL injection to najpoważniejsze zagrożenie bezpieczeństwa przy pracy z bazami danych. Powstaje, gdy nieoczyszczone dane wejściowe od użytkownika stają się częścią zapytania SQL. Atakujący może wstawić własne polecenia SQL i tym samym:
- Uzyskać nieautoryzowany dostęp do danych
- Zmodyfikować albo usunąć dane w bazie
- Obejść uwierzytelnianie
// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Atakujący może wpisać wartość taką jak: ' OR '1'='1
// Wynikowe zapytanie brzmiałoby: SELECT * FROM users WHERE name = '' OR '1'='1'
// Co zwróci wszystkich użytkowników
To samo dotyczy Database Explorera:
// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Zapytania parametryzowane
Podstawową obroną przed SQL injection są zapytania parametryzowane. Nette Database oferuje kilka sposobów ich użycia.
Najprostszy sposób to użycie zastępników w postaci znaku zapytania:
// ✅ Bezpieczne zapytanie parametryzowane
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Bezpieczny warunek w Explorerze
$table->where('name = ?', $name);
Dotyczy to wszystkich pozostałych metod w Database Explorerze, które pozwalają wstawiać wyrażenia z zastępnikami i parametrami.
Dla poleceń INSERT, UPDATE albo klauzuli WHERE możemy przekazać wartości w tablicy:
// ✅ Bezpieczny INSERT
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ Bezpieczny INSERT w Explorerze
$table->insert([
'name' => $name,
'email' => $email,
]);
Walidacja wartości parametrów
Zapytania parametryzowane są kamieniem węgielnym bezpiecznej pracy z bazą danych. Wartości, które do nich wstawiamy, muszą jednak przejść kilka poziomów kontroli:
Kontrola typów
Najważniejsze jest zapewnienie poprawnego typu danych parametrów – to warunek konieczny bezpiecznego użycia Nette Database. Baza danych zakłada, że wszystkie dane wejściowe mają poprawny typ danych odpowiadający danej kolumnie.
Gdyby na przykład $name z poprzednich przykładów było nieoczekiwanie tablicą zamiast ciągiem, Nette
Database spróbowałoby wstawić do zapytania SQL wszystkie jej elementy, co doprowadziłoby do błędu. Dlatego nigdy nie
używaj niezwalidowanych danych z $_GET, $_POST czy $_COOKIE bezpośrednio w
zapytaniach do bazy danych.
Walidacja formatu
Na drugim poziomie sprawdzamy format danych, na przykład czy ciągi są w kodowaniu UTF-8 i czy ich długość odpowiada definicji kolumny albo czy wartości liczbowe mieszczą się w dozwolonym zakresie typu danych danej kolumny.
Na tym poziomie walidacji możemy częściowo polegać na samej bazie danych: wiele baz odrzuci nieprawidłowe dane. Zachowanie może się jednak różnić, niektóre mogą po cichu przyciąć długie ciągi albo obciąć liczby spoza zakresu.
Walidacja specyficzna dla domeny
Trzeci poziom to logiczne kontrole specyficzne dla Twojej aplikacji. Na przykład sprawdzenie, czy wartości z selectboxów odpowiadają oferowanym opcjom, czy liczby mieszczą się w oczekiwanym zakresie (np. wiek 0–150 lat) albo czy wzajemne zależności między wartościami mają sens.
Zalecane sposoby walidacji
- Używaj Nette Forms, które automatycznie zapewniają właściwą walidację wszystkich wejść.
- Używaj presenterów i podawaj typy danych parametrów w metodach
action*()irender*(). - Albo zaimplementuj własną warstwę walidacyjną za pomocą standardowych narzędzi PHP, jak
filter_var().
Bezpieczna praca z kolumnami
W poprzedniej sekcji pokazaliśmy, jak poprawnie walidować wartości parametrów. Przy używaniu tablic w zapytaniach SQL musimy jednak poświęcić taką samą uwagę ich kluczom.
// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone
$database->query('INSERT INTO users', $_POST);
Przy poleceniach INSERT i UPDATE jest to krytyczna luka bezpieczeństwa: atakujący może wstawić albo zmodyfikować dowolną
kolumnę w bazie danych. Mógłby na przykład ustawić is_admin = 1 albo wstawić dowolne dane do wrażliwych kolumn
(tak zwana Mass Assignment Vulnerability).
W warunkach WHERE jest to jeszcze niebezpieczniejsze, bo mogą one zawierać operatory:
// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// wykona zapytanie WHERE (`salary` > 100000)
Atakujący może w ten sposób systematycznie odkrywać pensje pracowników. Może zacząć na przykład od zapytania o pensje powyżej 100 000, potem poniżej 50 000, a stopniowo zawężając zakres, może ujawnić przybliżone pensje wszystkich pracowników. Ten typ ataku nazywa się SQL enumeration.
Metody where() i whereOr() są nawet znacznie bardziej elastyczne i wspierają w kluczach oraz
wartościach wyrażenia SQL wraz z operatorami i funkcjami. Daje to atakującemu możliwość przeprowadzenia SQL injection:
// ❌ NIEBEZPIECZNY KOD - atakujący może wstawić własny SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// wykona zapytanie WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Ten atak kończy pierwotny warunek za pomocą 0), dokleja przez UNION własny SELECT,
żeby uzyskać wrażliwe dane z tabeli users, i zamyka składniowo poprawne zapytanie za pomocą
WHERE (1).
Whitelista kolumn
Do bezpiecznej pracy z nazwami kolumn potrzebujemy mechanizmu, który zapewni, że użytkownik może pracować tylko z dozwolonymi kolumnami i nie może dodać własnych. Moglibyśmy spróbować wykrywać i blokować niebezpieczne nazwy kolumn (blacklista), ale to podejście jest zawodne: atakujący zawsze może wymyślić nowy sposób zapisania niebezpiecznej nazwy kolumny, którego nie przewidzieliśmy.
Dlatego znacznie bezpieczniej jest odwrócić logikę i zdefiniować jawną listę dozwolonych kolumn (whitelistę):
// Kolumny, które użytkownik może modyfikować
$allowedColumns = ['name', 'email', 'active'];
// Usuwamy z wejścia wszystkie nieuprawnione kolumny
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Teraz bezpiecznie użyjemy w zapytaniach, na przykład:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Dynamiczne identyfikatory
Dla dynamicznych nazw tabel i kolumn używaj zastępnika ?name. Zapewnia on poprawne escapowanie identyfikatorów
zgodnie ze składnią danej bazy danych (np. za pomocą backticków w MySQL):
// ✅ Bezpieczne użycie zaufanych identyfikatorów
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Wynik w MySQL: SELECT `name` FROM `users`
Ważne: symbolu ?name używaj wyłącznie dla zaufanych wartości zdefiniowanych w kodzie aplikacji. Dla wartości
od użytkownika użyj ponownie whitelisty. W przeciwnym razie narażasz się na zagrożenia
bezpieczeństwa:
// ❌ NIEBEZPIECZNE - nigdy nie używaj wejścia od użytkownika
$database->query('SELECT ?name FROM users', $_GET['column']);