Zagrożenia bezpieczeństwa

Bazy danych często zawierają wrażliwe dane i umożliwiają wykonywanie niebezpiecznych operacji. Dla bezpiecznej pracy z Nette Database kluczowe są następujące aspekty:

  • Zrozumienie różnicy między bezpiecznym i niezabezpieczonym API
  • Używanie sparametryzowanych zapytań
  • Właściwa walidacja danych wejściowych

Czym jest SQL Injection?

Wstrzyknięcie kodu SQL jest najpoważniejszym zagrożeniem bezpieczeństwa podczas pracy z bazami danych. Występuje, gdy niefiltrowane dane wejściowe użytkownika stają się częścią zapytania SQL. Atakujący może wstawić własne polecenia SQL i w ten sposób

  • wyodrębnić nieautoryzowane dane
  • zmodyfikować lub usunąć dane w bazie danych
  • Ominąć uwierzytelnianie
// NIEBEZPIECZNY KOD - podatny na wstrzyknięcie kodu SQL
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Atakujący może wprowadzić wartość typu ' OR '1'='1
// Wynikowe zapytanie brzmiałoby SELECT * FROM users WHERE name = '' OR '1'='1'
// Które zwraca wszystkich użytkowników

To samo dotyczy eksploratora baz danych:

// NIEBEZPIECZNY KOD - podatny na wstrzyknięcie kodu SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Bezpieczne zapytania parametryzowane

Podstawową obroną przed SQL injection są sparametryzowane zapytania. Nette Database zapewnia kilka sposobów ich wykorzystania.

Najprostszym sposobem jest użycie znaków zapytania:

// Bezpieczne sparametryzowane zapytanie
$database->query('SELECT * FROM users WHERE name = ?', $name);

// Bezpieczny warunek w Eksploratorze
$table->where('name = ?', $name);

Dotyczy to wszystkich innych metod w Database Explorer, które umożliwiają wstawianie wyrażeń z symbolami zastępczymi znaków zapytania i parametrami.

W przypadku klauzul INSERT, UPDATE lub WHERE można przekazać wartości w tablicy:

// Bezpieczny INSERT
$database->query('INSERT INTO users', [
	'name' => $name,
	'email' => $email,
]);

// Bezpieczny INSERT w Eksploratorze
$table->insert([
	'name' => $name,
	'email' => $email,
]);

Walidacja wartości parametrów

Zapytania parametryzowane są podstawą bezpiecznej pracy z bazami danych. Jednak wartości przekazywane do nich muszą przejść kilka poziomów walidacji:

Sprawdzanie typu

Zapewnienie prawidłowego typu danych parametrów jest krytyczne – jest to warunek konieczny do bezpiecznego korzystania z Nette Database. Baza danych zakłada, że wszystkie dane wejściowe mają prawidłowy typ danych odpowiadający kolumnie.

Na przykład, jeśli $name w poprzednich przykładach nieoczekiwanie stał się tablicą zamiast łańcuchem, Nette Database spróbuje wstawić wszystkie jego elementy do zapytania SQL, co spowoduje błąd. Dlatego nigdy nie używaj niezwalidowanych danych z $_GET, $_POST lub $_COOKIE bezpośrednio w zapytaniach do bazy danych.

Walidacja formatu

Drugi poziom sprawdza format danych – na przykład upewniając się, że ciągi są zakodowane w UTF-8, a ich długość jest zgodna z definicją kolumny lub sprawdzając, czy wartości liczbowe mieszczą się w dopuszczalnym zakresie dla typu danych kolumny.

Na tym poziomie można częściowo polegać na samej bazie danych – wiele baz danych odrzuca nieprawidłowe dane. Jednak zachowanie może się różnić: niektóre mogą cicho obcinać długie ciągi lub przycinać liczby, które są poza zakresem.

Walidacja specyficzna dla domeny

Trzeci poziom obejmuje kontrole logiczne specyficzne dla danej aplikacji. Na przykład sprawdzenie, czy wartości z pól wyboru pasują do dostępnych opcji, czy liczby mieszczą się w oczekiwanym zakresie (np. wiek 0–150 lat) lub czy relacje między wartościami mają sens.

  • Użyj Nette Forms, które automatycznie obsługują prawidłową walidację wszystkich danych wejściowych.
  • Użyj Presenters i zadeklaruj typy danych parametrów w metodach action*() i render*().
  • Lub zaimplementować niestandardową warstwę walidacji przy użyciu standardowych narzędzi PHP, takich jak filter_var().

Bezpieczna praca z kolumnami

W poprzedniej sekcji omówiliśmy, jak prawidłowo walidować wartości parametrów. Jednak w przypadku korzystania z tablic w zapytaniach SQL, taką samą uwagę należy zwrócić na ich klucze.

// NIEBEZPIECZNY KOD - klucze tablicy nie są oczyszczane
$database->query('INSERT INTO users', $_POST);

W przypadku poleceń INSERT i UPDATE jest to poważna luka w zabezpieczeniach – atakujący może wstawić lub zmodyfikować dowolną kolumnę w bazie danych. Może na przykład ustawić is_admin = 1 lub wstawić dowolne dane do wrażliwych kolumn (znane jako Mass Assignment Vulnerability).

W warunkach WHERE jest to jeszcze bardziej niebezpieczne, ponieważ mogą one zawierać operatory:

// NIEBEZPIECZNY KOD - klucze tablicy nie są oczyszczane
$_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. Mogą zacząć od zapytania o pensje powyżej 100 000, następnie poniżej 50 000, a stopniowo zawężając zakres, mogą ujawnić przybliżone pensje wszystkich pracowników. Ten rodzaj ataku nazywany jest wyliczaniem SQL.

Metody where() i whereOr() są jeszcze bardziej elastyczne i obsługują wyrażenia SQL, w tym operatory i funkcje, zarówno w kluczach, jak i wartościach. Daje to atakującemu możliwość wykonania złożonego wstrzyknięcia SQL:

// NIEBEZPIECZNY KOD - atakujący może wstawić własny kod 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)

Atak ten kończy oryginalny warunek za pomocą 0), dołącza własny SELECT za pomocą UNION w celu uzyskania wrażliwych danych z tabeli users i zamyka poprawnym składniowo zapytaniem za pomocą WHERE (1).

Biała lista kolumn

Do bezpiecznej pracy z nazwami kolumn potrzebny jest mechanizm, który zapewnia, że użytkownicy mogą wchodzić w interakcje tylko z dozwolonymi kolumnami i nie mogą dodawać własnych. Próba wykrycia i zablokowania niebezpiecznych nazw kolumn (czarna lista) jest zawodna – atakujący zawsze może wymyślić nowy sposób na napisanie niebezpiecznej nazwy kolumny, której nie przewidziałeś.

Dlatego znacznie bezpieczniej jest odwrócić logikę i zdefiniować jawną listę dozwolonych kolumn (whitelisting):

// Kolumny, które użytkownik może modyfikować
$allowedColumns = ['name', 'email', 'active'];

// Usuń wszystkie nieautoryzowane kolumny z danych wejściowych
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));

// Teraz można bezpiecznie używać w zapytaniach, takich jak:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);

Dynamiczne identyfikatory

W przypadku dynamicznych nazw tabel i kolumn należy użyć symbolu zastępczego ?name. Zapewnia to prawidłową ucieczkę identyfikatorów zgodnie z podaną składnią bazy danych (np. przy użyciu backticks w MySQL):

// Bezpieczne korzystanie z 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 należy używać tylko w przypadku zaufanych wartości zdefiniowanych w kodzie aplikacji. W przypadku wartości dostarczonych przez użytkownika należy ponownie użyć białej listy. W przeciwnym razie istnieje ryzyko wystąpienia luk w zabezpieczeniach:

// NIEBEZPIECZEŃSTWO - nigdy nie używaj danych wejściowych użytkownika
$database->query('SELECT ?name FROM users', $_GET['column']);
wersja: 4.0