[MySQL][PHP] Jak zabezpieczyć bazę danych przed włamaniem za pomocą dopisania fragmentu kodu w formularzu?
Chcesz stworzyć bezpieczne połączenie z dowolną bazą danych, aby uniknąć ataku na serwer, przejęcia lub zniszczenia danych z bazy.
Niewiele osób zdaje sobie sprawę, jak łatwo można oszukać wiele skryptów i formularzy, które odwołują się do baz danych. Źle napisane procedury do odczytu rekordów, autoryzacji czy wybierania danych są wystarczającym narzędziem, dzięki któremu można wykraść dane, a także wykonać polecenia umożliwiające w skrajnych przypadkach przejęcie kontroli nad bazą danych lub nawet całym serwerem.
Większość osób korzysta z baz danych nieświadomie, nie wie więc, że pewna konstrukacja zapytań może powodować bardzo poważne szkody. Zobacz jak wygląda atak na bazę danych i jak można zabezpieczyć się przed takimi włamaniami.
Załóżmy sobie przykładową bazę danych. Niech zawiera dwa pola - imię i nazwisko. Wpiszmy też kilka rekordów...
imie nazwisko
Jan1 Kowalski1
Jan2 Kowalski2
Jan3 Kowalski3
Jan4 Kowalski4
Jan5 Kowalski5
Zamiast imienia i nazwiska może to być hasło i login oczywiście. Mamy też formularz, który umożliwia wpisanie imienia. Po wpisaniu prawidłowego imienia zobaczymy odpowiadające mu nazwisko. Przykład prosty, ale czytelny. Sprawdźmy jak można odczytać wszystkie imiona i nazwiska bez znajomości imion. Po prostu dokonamy "włamania"...
Jak się włamać?
W PHP zapytanie może wyglądać w poniższy sposób, gdzie zmienna $pole1 zawiera wartość wpisaną do formularza.
<?
$sql="SELECT * FROM ludzie where imie='$pole1'";
?>
Zamiast budować formularz, możemy od razu wywołać stronę zawierającą wartość tego pola poprzez przekazanie parametru z wartością:
http://strona.com/index.php?pole1=jan1
Wynik jest prosty do przewidzenia:
<?
$sql="SELECT * FROM ludzie where imie='jan1'";
?>
czyli pojawi się nam jeden rekord:
Jan1 Kowalski1
Jeżeli będziemy wpisywać inne imiona niż są podane w bazie danych nie zobaczymy żadnych rekordów. Wystarczy jednak zmodyfikować nieco zapytanie, aby zobaczyć zawartość całej bazy! Tym razem zamiast imienia napiszmy w zapytaniu/formularzu coś takiego:
' or 1=1 --'
lub coś takiego:
' or 1=1 #
a więc pytanie będzie wyglądać tak:
index.php?pole1='%20or%201=1%23
Wygląda niewinnie (%20 to spacje, %23 to #), ale w zapytaniu SQL przybierze taką postać:
<?
$sql="SELECT * FROM ludzie where imie='' or 1=1 #';
?>
A to nie jest już niewinne zapytanie, ponieważ wybierze z bazy (a może nawet wyświetli na ekranie) wszystkie rekordy...
Dodatkowy kod działa bardzo prosto - zamyka cudzysłów dla pola imie, dodaje warunek logiczny or 1=1, który zawsze jest spełniony, więc baza danych zwraca wszystkie rekordy, a na końcu za pomocą znaku # traktuje dalsze pytanie jako komentarz i ignoruje zamknięcie średnika. W innych bazach komentarzem może być np. -- (dwa średniki).
Oczywiście atak nie jest możliwy na każdą bazę danych i w każdych warunkach. PHP automatycznie dodaje do każdego cudzysłowu poprzedzający go slash \, więc jest on niegroźny i traktowany jako tekst.
Jeżeli jednak korzystasz z różnych funkcji zmieniających zawartość pola i usuwających slashe, chociażby wykorzystując stripslashes(), Twoja baza stoi przed poważnym zagrożeniem.
Ze względów bezpieczeństwa nie pokażę jak można przejąć kontrolę nad bazą danych, a w skrajnie niekorzystnych przypadkach jest to możliwe. Odczytując zawartość baz danych w łatwy sposób można poznać hasła, dane teleadresowe firm, numery kart kredytowych czy inne informacje, które nie powinny być dostępne dla zwykłego użytkownika. W efekcie niewinny błąd może spowodować bardzo poważne konsekwencje i ogromne straty.
Jak się bronić?
Opisany atak jest groźny tylko wtedy gdy usuwamy slashe, więc przed wysłaniem zapytania do bazy danych należy znowu dodać ukośniki przed cydzysłowami za pomocą funkcji addslashes():
<?
$pole1 = addslashes($pole1);
?>
Przede wszystkim pytania należy przetestować. A więc wyświetlić na ekranie, jak będzie wyglądało zapytanie w ostatecznej formie i jakie dane zwróci baza. Oczywiście test polega na wpisaniu innych niż spodziewane parametry, aby przekonać się czy baza jest odporna chociażby na podane kombinacje znaków:
' or 1=1 #
" or 1=1 #
or 1=1 #
' or 1=1 --
" or 1=1 --
" or 1=1 --"
or 1=1 --
' or 'a'='a
" or "a"="a
') or ('a'='a
Kolejną sprawą jest sprawdzenie typów danych. Jeżeli oczekujesz np. cyfr, a ktoś wpisuje litery, nie powinno zostać wykonane zapytanie SQL. Podobnie gdy ktoś wpisuje liczby, a oczekujesz danych tekstowych (bez liczb).
<?
$dane=$_GET["pole1"];
if (ereg("^-?[0-9][0-9]*(\.[0-9])?[0-9]*$",$dane)) echo "To jest liczba!";
else echo "To nie jest liczba!";
?>
Powyższe wyrażenie rozróżnia liczby całkowite i rzeczywiste, również ze znakiem minus.
W prosty sposób można pobrane z formularza wartości przekonwertować do określonego typu za pomocą rzutowania typów, np. dla integer będzie to (int).
<?
$liczba = (int) $_GET["pole1"]; // pole1 = "123de5"
echo $liczba; // wynik: 123
$liczba = (int) $_GET["pole1"]; // pole1 = "1235"
echo $liczba; // wynik: 1235
$liczba = (int) $_GET["pole1"]; // pole1 = "123.91"
echo $liczba; // wynik: 123
?>
Łatwo można sie też upewnić, czy dana jest określonego typu, przykładem również niech będzie liczba typu integer.
<?
$liczba=1223.17;
if (is_int($liczba)) echo "To jest liczba typu integer";
else echo "To nie jest liczba typu integer!";
?>
Jeżeli to możliwe, niech w pytaniu SQL nie znajdują sie parametry wprowadzane z formularzy. Możesz odseparować pytanie od danych z formularza za pomocą instrukcji warunkowych (oczywiście, jeżeli kombinacji nie jest wiele):
<?
if ($pole1=="czerwony") $sql="SELECT * FROM auta where kolor='czerwony'";
if ($pole1=="czarny") $sql="SELECT * FROM auta where kolor='czarny'";
?>
Możesz też kasować z zapytań SQL następujące ciągi znaków:
'
"
;
#
--
select
drop
insert
delete
Taka ochrona powinna dać Ci poczucie bezpieczeństwa i uodpornić bazę danych na opisaną formę ataku.