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*()render*()
  • 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']);
wersja: 4.0