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.
Zalecane metody walidacji
- 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*()
irender*()
. - 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']);