Ryzyka bezpieczeństwa
Baza danych często zawiera wrażliwe dane i umożliwia wykonywanie niebezpiecznych operacji. Dla bezpiecznej pracy z Nette Database kluczowe jest:
- Zrozumienie różnicy między bezpiecznym a niebezpiecznym API
- Używanie sparametryzowanych zapytań
- Poprawna walidacja danych wejściowych
Co to jest SQL Injection?
SQL injection jest najpoważniejszym ryzykiem bezpieczeństwa podczas pracy z bazą danych. Powstaje, gdy nieprzetworzone dane wejściowe od użytkownika stają się częścią zapytania SQL. Atakujący może wstrzyknąć własne polecenia SQL i tym samym:
- Uzyskać nieautoryzowany dostęp do danych
- Zmodyfikować lub usunąć dane w bazie danych
- Ominąć uwierzytelnianie
// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Atakujący może podać na przykład wartość: ' OR '1'='1
// Wynikowe zapytanie będzie wtedy: SELECT * FROM users WHERE name = '' OR '1'='1'
// Co zwróci wszystkich użytkowników
To samo dotyczy Database Explorer:
// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Zapytania sparametryzowane
Podstawową obroną przed SQL injection są zapytania sparametryzowane. Nette Database oferuje kilka sposobów ich użycia.
Najprostszym sposobem jest użycie symboli zastępczych (placeholderów) w postaci znaków zapytania:
// ✅ Bezpieczne zapytanie sparametryzowane
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Bezpieczny warunek w Explorerze
$table->where('name = ?', $name);
Dotyczy to wszystkich innych metod w Database Explorer, które umożliwiają wstawianie wyrażeń z symbolami zastępczymi i parametrami.
Dla poleceń INSERT, UPDATE lub 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 sparametryzowane są podstawowym elementem bezpiecznej pracy z bazą danych. Jednak wartości, które do nich wstawiamy, muszą przejść przez kilka poziomów kontroli:
Kontrola typów
Najważniejsze jest zapewnienie poprawnego typu danych parametrów – jest to warunek konieczny do bezpiecznego używania Nette Database. Baza danych zakłada, że wszystkie dane wejściowe mają poprawny typ danych odpowiadający danej kolumnie.
Na przykład, jeśli $name
w poprzednich przykładach byłoby niespodziewanie tablicą zamiast stringiem, Nette
Database próbowałoby wstawić wszystkie jej elementy do zapytania SQL, co doprowadziłoby do błędu. Dlatego nigdy nie
używaj niezweryfikowanych danych z $_GET
, $_POST
lub $_COOKIE
bezpośrednio w
zapytaniach bazodanowych.
Kontrola formatu
Na drugim poziomie kontrolujemy format danych – na przykład, czy ciągi znaków są w kodowaniu UTF-8 i ich długość odpowiada definicji kolumny, lub czy wartości liczbowe mieszczą się w dozwolonym zakresie dla danego typu danych kolumny.
Na tym poziomie walidacji możemy częściowo polegać na samej bazie danych – wiele baz danych odrzuci nieprawidłowe dane. Jednak zachowanie może się różnić, niektóre mogą cicho skrócić długie ciągi znaków lub przyciąć liczby spoza zakresu.
Kontrola domenowa
Trzeci poziom stanowią kontrole logiczne specyficzne dla Twojej aplikacji. Na przykład weryfikacja, czy wartości z pól wyboru odpowiadają oferowanym opcjom, czy liczby mieszczą się w oczekiwanym zakresie (np. wiek 0–150 lat) lub czy wzajemne zależności między wartościami mają sens.
Zalecane sposoby walidacji
- Używaj Formularzy Nette, które automatycznie zapewniają poprawną walidację wszystkich danych wejściowych
- Używaj Presenterów i podawaj typy danych dla parametrów w metodach
action*()
irender*()
- Lub zaimplementuj własną warstwę walidacji za pomocą standardowych narzędzi PHP, takich
jak
filter_var()
Bezpieczna praca z kolumnami
W poprzedniej sekcji pokazaliśmy, jak poprawnie walidować wartości parametrów. Jednak przy użyciu tablic w zapytaniach SQL musimy poświęcić taką samą uwagę ich kluczom.
// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są sprawdzane
$database->query('INSERT INTO users', $_POST);
W przypadku poleceń INSERT i UPDATE jest to fundamentalny błąd bezpieczeństwa – atakujący może wstawić lub zmienić
dowolną kolumnę w bazie danych. Mógłby na przykład ustawić is_admin = 1
lub wstawić dowolne dane do
wrażliwych kolumn (tzw. Mass Assignment Vulnerability).
W warunkach WHERE jest to jeszcze bardziej niebezpieczne, ponieważ mogą zawierać operatory:
// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są sprawdzane
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// wykonuje zapytanie WHERE (`salary` > 100000)
Atakujący może wykorzystać to podejście do systematycznego odkrywania wynagrodzeń pracowników. Zacznie na przykład od zapytania o wynagrodzenia powyżej 100 000, następnie poniżej 50 000 i stopniowo zawężając zakres, może odkryć przybliżone wynagrodzenia wszystkich pracowników. Ten typ ataku nazywa się SQL enumeration.
Metody where()
i whereOr()
są jeszcze znacznie bardziej elastyczne i obsługują w kluczach
i wartościach wyrażenia SQL, w tym operatory i funkcje. Daje to atakującemu możliwość przeprowadzenia SQL injection:
// ❌ NIEBEZPIECZNY KOD - atakujący może wstrzyknąć własny SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// wykonuje zapytanie WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Ten atak kończy pierwotny warunek za pomocą 0)
, dołącza własne SELECT
za pomocą
UNION
, aby uzyskać wrażliwe dane z tabeli users
i zamyka składniowo poprawne zapytanie za pomocą
WHERE (1)
.
Biała lista 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 próbować wykrywać i blokować niebezpieczne nazwy kolumn (czarna lista), ale to podejście jest zawodne – atakujący zawsze może wymyślić nowy sposób zapisu niebezpiecznej nazwy kolumny, którego nie przewidzieliśmy.
Dlatego znacznie bezpieczniejsze jest odwrócenie logiki i zdefiniowanie jawnej listy dozwolonych kolumn (biała lista):
// Kolumny, które użytkownik może edytować
$allowedColumns = ['name', 'email', 'active'];
// Usuwamy wszystkie niedozwolone kolumny z danych wejściowych
$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // array_flip for PHP < 8.1
// ✅ Teraz możemy bezpiecznie używać 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żyj symbolu zastępczego ?name
. Zapewni on poprawne escapowanie
identyfikatorów zgodnie ze składnią danej bazy danych (np. za pomocą odwrotnych apostrofó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: symbol ?name
używaj tylko dla zaufanych wartości zdefiniowanych w kodzie aplikacji. Dla wartości od
użytkownika użyj ponownie białej listy. W przeciwnym razie narażasz się na ryzyko
bezpieczeństwa:
// ❌ NIEBEZPIECZNE - nigdy nie używaj danych wejściowych od użytkownika
$database->query('SELECT ?name FROM users', $_GET['column']);