Bezpieczeństwo baz danych
W dzisiejszych czasach bazy danych są kluczowymi komponentami każdej aplikacji internetowej,
umożliwiającymi stronom internetowym dostarczanie różnych dynamicznych treści. Ponieważ w bazie danych
mogą być przechowywane bardzo wrażliwe lub tajne informacje, należy poważnie podejść
do kwestii ochrony swoich baz danych.
Aby pobrać lub zapisać jakiekolwiek informacje, należy się połączyć do bazy danych,
wysłać poprawne zapytanie, pobrać wynik i zamknąć połączenie.
W obecnych czasach powszechnie stosowanym w tym celu językiem zapytań jest
SQL (Structured Query Language). Zobacz, w jaki sposób atakujący może manipulować zapytaniami SQL.
Jak możesz przypuszczać, PHP nie może ochronić Twojej bazy danych samej w sobie.
Kolejne sekcje są wprowadzeniem do podstaw tego, jak
uzyskiwać dostęp i zmieniać informacje w bazach danych z poziomu skryptów PHP.
Pamiętaj o prostej zasadzie: głęboka ochrona. W im większej ilości miejsc
zadbasz o zwiększenie ochrony swojej bazy danych, tym mniejsze
prawdopodobieństwo, że atakujący da radę wykraść lub wykorzystać przechowywane
w niej informacje. Dobry projekt schematu bazy danych oraz aplikacji
poradzi sobie z większością problemów.
Projektowanie baz danych
Pierwszym krokiem jest zawsze stworzenie bazy danych, chyba że chcesz
użyć bazy danych od strony trzeciej. Gdy baza danych jest tworzona,
przypisywany jest jej właściciel, który wykonał żądanie tworzące bazę. Zazwyczaj tylko
właściciel (lub superużytkownik) może operować na obiektach w tej
bazie danych, a inni użytkownicy muszą otrzymać uprawnienia,
aby mogli ich używać.
Aplikacja nigdy nie powinna łączyć się do bazy danych jako jej właściciel lub
superużytkownik, ponieważ ci użytkownicy mogą wykonać dowolne zapytania, na
przykład zmieniające strukturę bazy danych (np. usuwające tabele) lub
usuwające całą jej zawartość.
Możesz stworzyć różnych użytkowników bazy danych dla każdego z zastosowań
Twojej aplikacji z bardzo ograniczonymi uprawnieniami do obiektów w bazie danych.
Powinieneś nadawać tylko niezbędne uprawnienia i unikać tego, by ten sam użytkownik
mógł używać bazy danych w innych celach. Oznacza to, że jeśli
atakujący zdobędą dostęp do Twojej bazy danych używając poświadczeń aplikacji,
mogą wykonać tylko takie zmiany, które może wykonać Twoja aplikacja.
Łączenie z bazą danych
Możesz ustanowić połączenie z wykorzystaniem SSL aby zaszyfrować
komunikację między klientem a serwerem i zwiększyć bezpieczeństwo, lub użyć ssh,
aby zaszyfrować połączenie sieciowe między klientami a serwerem bazy danych.
Przy zastosowaniu któregoś z tych rozwiązań monitorowanie ruchu i zdobycie
informacji o Twojej bazie danych będzie utrudnione dla potencjalnego atakującego.
Szyfrowany model zapisu
SSL lub SSH chroni dane płynące z klienta do serwera, ale
nie chroni danych już zapisanych w bazie. SSL jest
protokołem szyfrującym transfer danych.
Kiedy atakujący uzyska bezpośredni dostęp do Twojej bazy danych (omijając
serwer WWW), przechowywane wrażliwe informacje mogą być ujawnione, chyba że
informacja jest chroniona przez samą bazę danych. Szyfrowanie danych
jest dobrym sposobem, aby zapobiec temu zagrożeniu, ale bardzo niewiele
baz danych oferuje ten typ szyfrowania danych.
Najprostszym sposobem obejścia tego problemu jest stworzenie
swojej własnej paczki szyfrującej i wykorzystanie jej z poziomu skryptów PHP. PHP
może Ci w tym pomóc dostarczając kilka rozszerzeń, takich jak OpenSSL i Sodium, które oferują szeroką gamę algorytmów
szyfrujących. Skrypt może szyfrować dane przed ich umieszczeniem w bazie danych i odszyfrować
je w chwili odczytu. Zobacz odniesienia, żeby zobaczyć dalsze przykłady tego,
jak działa szyfrowanie.
Haszowanie
W wypadku naprawdę ukrytych danych, jeśli ich surowa reprezentacja nie jest potrzebna
(tj. dane nie będą wyświetlane), można rozważyć ich haszowanie.
Powszechnie znanym przykładem haszowania jest zapisywanie haszu kryptograficznego
haseł w bazie danych, zamiast trzymania samych haseł.
Funkcje modułu password
oferują wygodny sposób haszowania wrażliwych danych i pracy z tymi haszami.
Funkcja password_hash jest używana do haszowania podanego ciągu znaków używając
najmocniejszego obecnie dostępnego algorytmu, a password_verify
sprawdza czy podane hasło pasuje do haszu zapisanego w bazie danych.
Haszowanie pola z hasłem
]]>
SQL Injection (wstrzyknięcie SQL)
SQL injection (wstrzyknięcie SQL) jest techniką, w której atakujący wykorzystuje luki
w kodzie aplikacji odpowiedzialnym za budowanie dynamicznych zapytań SQL.
Atakujący może zyskać dostęp do chronionych części aplikacji,
pobrać wszystkie informacje z bazy danych, manipulować istniejącymi danymi
lub nawet wykonywać niebezpieczne komendy systemowe na maszynie bazy
danych. Podatność występuje, gdy programista łączy lub wplata
dane wejściowe od użytkownika do zapytań SQL.
Dzielenie wyników na strony... i tworzenie superużytkowników
(PostgreSQL)
W następującym przykładzie dane wejściowe od użytkownika są bezpośrednio wplatane
w zapytanie SQL, pozwalając atakującemu na zdobycie konta superużytkownika w bazie danych.
]]>
Normalni użytkownicy klikają na linki 'następna'/'poprzednia', które kodują wartość
zmiennej $offset w adresie URL. Skrypt spodziewa się, że
dane przychodzące w zmiennej $offset są liczbą. Jednak co jeśli ktoś spróbuje
włamać się doklejając następującą rzecz do adresu URL?
Jeśli tak się stanie, skrypt utworzyłby atakującemu konto superużytkownika.
Zauważ, że 0; zostało podane aby dostarczyć poprawny offset
do oryginalnego zapytania i je zakończyć.
Częstą techniką wymuszenia na parserze SQL aby zignorował resztę
zapytania napisanego przez programistę jest podanie --, czyli
znaków rozpoczynających komentarz w SQL.
Możliwym sposobem na uzyskanie haseł jest obejście stron wyników wyszukiwania.
Jedynym co musi zrobić atakujący jest zobaczenie czy istnieją jakieś wysyłane zmienne
używane w zapytaniu SQL, które nie są obsługiwane poprawnie. Takie filtry mogą być
często ustawiane w poprzedzającym formularzu, aby dostosować klauzule WHERE, ORDER BY,
LIMIT and OFFSET w zapytaniu SELECT.
Jeśli Twoja baza danych wspiera konstrukcję UNION,
atakujący może spróbować dokleić całe zapytanie do oryginalnego, aby otrzymać
listę haseł z określonej tabeli. Jest zdecydowanie zalecane aby przetrzymywać jedynie
bezpieczne hasze haseł zamiast samych haseł.
Pobieranie listy artykułów... i haseł (dowolna baza danych)
]]>
Statyczna część zapytania może zostać połączona z innym
wyrażeniem SELECT, które ujawnia wszystkie hasła:
Zapytania UPDATE i INSERT również są
podatne na takie ataki.
Formularz resetujący hasło... aby uzyskać więcej uprawnień (dowolna baza danych)
]]>
Jeżeli złośliwy użytkownik wyśle wartość
' or uid like'%admin% jako zmienną $uid, aby
zmienić hasło administratora lub po prostu ustawi $pwd na
hehehe', trusted=100, admin='yes aby uzyskać więcej
uprawnień, to zapytanie zostanie zmienione:
]]>
Mimo, że jest oczywiste, że atakujący musi posiąść jakąś
wiedzę na temat architektury bazy danych, aby przeprowadzić udany
atak, to zdobycie tej informacji jest często bardzo proste. Przykładowo
kod może być częścią otwartoźródłowego oprogramowania i być publicznie dostępny.
Informacje te mogą również zostać ujawnione
przez zamknięty kod źródłowy - nawet jeśli jest on zakodowany, zaciemniony lub skompilowany -
a nawet przez Twój własny kod, poprzez wyświetlanie wiadomości błędów.
Inne metody to np. użycie typowych nazw tabel i kolumn Na
przykład formularz logowania, który używa taeli 'users' z kolumnami nazwanymi
'id', 'username', and 'password'.
Atak na system operacyjny serwera baz danych (MSSQL Server)
Przerażający przykład tego, jak na niektórych maszynach baz danych można
uzyskać dostęp do komend systemowych.
]]>
Jeśli atakujący wyśle wartość
a%' exec master..xp_cmdshell 'net user test testpass /ADD' --
jako zmienną $prod, to wartością $query będzie:
]]>
MSSQL Server wykonuje zapytania SQL w grupach, w tym komendę
dodającą nowego użytkownika do lokalnych kont bazy danych. Jeśli ta aplikacja
była uruchomiona jako sa i usługa MSSQLSERVER została
uruchomiona z wystarczającymi uprawnieniami, to atakujący miałby teraz
konto z dostępem do tej maszyny.
Niektóre przykłady powyżej są powiązane z konkretnym serwerem baz danych, ale
nie oznacza to, że podobne ataki są niemożliwe względem innych produktów.
Twój serwer baz danych może mieć podobną podatność wykorzystywaną w inny sposób.
Zabawny przykład problemów związanych z SQL injection
Obrazek dzięki uprzejmości xkcd
Techniki unikania
Rekomendowanym sposobem unikania SQL injection jest przypisanie wszystkich danych
za pomocą przygotowanych instrukcji (ang. prepared statements). Użycie parametryzowanych zapytań
nie jest wystarczające aby zupełnie uniknąć SQL injection, ale jest najbezpieczniejszym sposobem dostarczenia
danych wejściowych od użytkownika do zapytań SQL. Wszystkie dynamiczne dane w zapytaniach WHERE,
SET i VALUES muszą być
zastąpione symbolami zastępczymi (placeholderami). Rzeczywiste dane zostaną podstawione podczas
wykonywania i zostaną wysłane osobno od zapytania SQL.
Podstawianie parametrów może być wykorzystane tylko dla danych. Inne dynamiczne części
zapytań SQL muszą być filtrowane tak, aby pozwolić tylko na znaną listę dozwolonych wartości.
Unikanie SQL injection przy użyciu przygotowanych instrukcji PDO
prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// Wartość jest dostarczana z symbolami wieloznacznymi LIKE
$stmt->execute(["%{$productId}%"]);
?>
]]>
Przygotowane instrukcje są obsługiwane
przez PDO,
przez MySQLi
i inne biblioteki baz danych.
Ataki SQL injection są oparte głównie na wykorzystywaniu kodu, który nie został stworzony
z myślą o bezpieczeństwie. Nigdy nie ufaj żadnym danym wejściowym, szczególnie
pochodzącym od użytkownika, nawet jeśli pochodzi z listy wyboru,
ukrytego pola formularza lub ciasteczka. Pierwszy przykład pokazuje, że takie
proste zapytanie może spowodować prawdziwy chaos.
Strategia głębokiej ochrony opiera się na kilku dobrych praktykach kodowania:
Nigdy nie łącz się do bazy danych jako superużytkownik lub właściciel bazy danych.
Zawsze używaj dostosowanych użytkowników z minimalnymi uprawnieniami.
Sprawdzaj czy otrzymane dane wejściowe mają oczekiwany typ danych. PHP ma
szeroki zakres funkcji sprawdzających dane, od najprostszych, które
można znaleźć w funkcjach zmiennych i
w funkcjach typu znaków
(np. is_numeric czy ctype_digit)
aż po wsparcie
wyrażeń regularnych kompatybilnych z Perlem.
Jeżeli aplikacja oczekuje danych numerycznych, rozważ ich weryfikację
funkcją ctype_digit, po cichu zmień ich typ
używając settype lub użyj reprezentacji numerycznej
dzięki sprintf.
Jeżeli warstwa bazy danych nie wspiera podstawiania parametrów, to
ujmij w apostrofy każdą nienumeryczną wartość przekazaną przez użytkownika, która jest
przekazywana do bazy danych. Zrób to za pomocą funkcji dodającej znaki ucieczki dla Twojej bazy danych (np.
mysql_real_escape_string,
sqlite_escape_string, itd.).
Ogólne funkcje takie jak addslashes są przydatne tylko
w bardzo określonych zastosowaniach (np. MySQL w jednobajtowym zestawie znaków
z wyłączonym NO_BACKSLASH_ESCAPES), więc
lepiej ich unikać.
Nie wyświetlaj żadnych informacji o bazie danych, szczególnie
jej strukturze — czy to umyślnie, czy przypadkiem. Zobacz też sekcje raportowanie błędów and funkcje raportowania i logowania błędów.
Poza tym, warto też skorzystać z logowania zapytań w kodzie Twojej aplikacji
lub przez samą bazę danych, jeśli wspiera ona takie logi. Oczywiście logowanie danych nie
jest w stanie zapobiec szkodliwym sytuacjom, ale może być pomocne w wyśledzeniu, która
aplikacja została zaatakowana. Log nie jest przydatny sam w sobie, lecz
dzięki informacjom które zawiera. Ogólnie rzecz biorąc, im więcej szczegółów tym lepiej.