Ujawnione dane uwierzytelniające
Większość aplikacji PHP korzysta z baz danych. Najczęściej aplikacja łączy się z wybraną bazą, a następnie dokonuje w niej uwierzytelnienia przy użyciu danych uwierzytelniających:
<?php
$host = 'example.org';
$username = 'nazwaużytkownika';
$password = 'hasło';
$db = mysql_connect($host, $username, $password);
?>
Kod ten może znajdować się w pliku o nazwie db.inc
dołączanym do aplikacji zawsze, gdy potrzebne jest połączenie z bazą danych. Jest to wygodne rozwiązanie, które pozwala na przechowywanie danych uwierzytelniających w jednym pliku.
Jeśli jednak plik taki jest przechowywany w katalogu głównym dokumentów, mogą wystąpić pewne problemy. Technika ta jest popularna, ponieważ znacznie ułatwia korzystanie z instrukcji include
i require
, ale w pewnych sytuacjach może umożliwić ujawnienie danych uwierzytelniających.
Przypomnijmy, że wszystko, co znajduje się w katalogu głównym dokumentów ma określony adres URL. Jeśli na przykład ścieżką do katalogu głównego dokumentów jest /usr/local/apache/htdocs
, to plik znajdujący się w katalogu /usr/local/apache/htdocs/inc/db.inc
ma adres URL http://example.org/inc/db.inc
.
Biorąc pod uwagę fakt, że większość serwerów sieciowych pliki z rozszerzeniem .inc
serwuje jako zwykły tekst, nietrudno dostrzec, jak bardzo ta metoda jest niebezpieczna. Mówiąc ogólnie, istnieje ryzyko ujawnienia całości kodu znajdującego się w tych plikach, a ujawnienie danych uwierzytelniających jest po prostu wyjątkowo groźne.
Oczywiście prostym rozwiązaniem jest przeniesienie wszystkich tych modułów poza katalog główny i jest to bardzo dobry sposób. Instrukcje include
i require
przyjmują jako argumenty także ścieżki odnoszące się do systemu plików, a więc nie ma potrzeby udostępniać modułów poprzez URL. Jest to tylko niepotrzebne ryzyko.
Jeśli nie ma innej możliwości i pliki te muszą znajdować się w katalogu głównym, to można wpisać poniższe dyrektywy w pliku httpd.conf
(dotyczy serwera Apache):
<Files ~ ".inc$">
Order allow,deny
Deny from all
</Files>
Moduły nie powinny być przetwarzane przez mechanizm PHP. Dotyczy to zarówno zmiany ich rozszerzeń .php
jak i użycia dyrektyw AddType
, aby pliki z rozszerzeniem .inc
były traktowane jako pliki PHP. Wykonywanie kodu poza kontekstem może być bardzo niebezpieczne, ponieważ jest nieprzewidywalne i może zwracać niespodziewane wyniki. Jeśli jednak w modułach znajdują się np. tylko instrukcje przypisania wartości zmiennym, ryzyko to jest znacznie mniejsze.
Moja ulubiona metoda ochrony danych dostępu do baz danych jest opisana w książce PHP. Receptury. Wydanie II (Helion 2007) Davida Sklara i Adama Trachtenberga. Utwórz plik, /ścieżka/do/tajnych-danych
, który może odczytać tylko root
(nie nobody
):
SetEnv DB_USER "użytkownik"
SetEnv DB_PASS "hasło"
Zdefiniuj dołączanie tego pliku w pliku httpd.conf
w następujący sposób:
Include "/ścieżka/do/tajnych-danych"
Teraz możesz używać w swoim kodzie wartości $_SERVER['DB_USER']
i $_SERVER['DB_PASS']
. Nie tylko nigdy więcej nie będziesz musieć wpisywać nazwy użytkownika i hasła w swoich skryptach, lecz również serwer nie będzie mógł odczytać pliku tajnych-danych
, dzięki czemu nikt nie będzie mógł napisać skryptu odczytującego Twoje dane uwierzytelniające (niezależnie od języka). Uważaj tylko, aby nie ujawnić tych zmiennych za pomocą phpinfo()
, print_r($_SERVER)
albo czegoś innego w tym rodzaju.
SQL Injection
Obrona przed atakami typu SQL injection jest bardzo prosta, a mimo to wciąż są aplikacje, które są na nie podatne. Rozważmy poniższy przykład SQL:
<?php
$sql = "INSERT
INTO users (reg_username,
reg_password,
reg_email)
VALUES ('{$_POST['reg_username']}',
'$reg_password',
'{$_POST['reg_email']}')";
?>
Użyte w tym skrypcie zapytanie zawiera zmienną $_POST
, co od razu budzi podejrzenia.
Załóżmy, że tworzy ono nowe konto. Użytkownik podaje wybraną nazwę użytkownika i adres e-mail. Aplikacja rejestracyjna generuje tymczasowe hasło i wysyła je na podany adres e-mail w celu jego weryfikacji. Wyobraźmy sobie, że użytkownik wpisuje poniższą nazwę użytkownika:
bad_guy', 'mypass', ''), ('good_guy
Z pewnością to nie wygląda na poprawną nazwę użytkownika, ale jeśli nie zaimplementuje się żadnego filtru, to aplikacja nic nie zauważy. Jeśli zostanie podany poprawny adres e-mail (np. shiflett@php.net
), a aplikacja wygeneruje hasło 1234
, to powstanie następujące zapytanie SQL:
<?php
$sql = "INSERT
INTO users (reg_username,
reg_password,
reg_email)
VALUES ('bad_guy', 'mypass', ''), ('good_guy',
'1234',
'shiflett@php.net')"; ?>
Przez ten podstęp, zamiast utworzenia jednego konta (good_guy
) z poprawnym adresem e-mail, aplikacja utworzy dwa konta i użytkownik dostarczył wszystkie dane konta bad_guy
.
Mimo iż ten konkretny przykład wydaje się nieszkodliwy, jest jasne, że jeśli atakujący otrzyma możliwość modyfikowania zapytań SQL, to z pewnością wykorzysta to do znacznie gorszych celów.
Na przykład w niektórych bazach danych możliwe jest wysłanie wielu zapytań w jednym wywołaniu. Użytkownik może to wykorzystać kończąc istniejące zapytanie średnikiem i wpisując za nim własne zapytanie.
Baza danych MySQL do niedawna nie zezwalała na wykonywanie wielu zapytań na raz, a więc w jej przypadku ryzyka tego nie było. W nowszych wersjach tej bazy jest to jednak już możliwe, ale rozszerzenie PHP o nazwie (ext/mysqli
) wymaga, aby do wysyłania wielu żądań używać osobnej funkcji o nazwie (mysqli_multi_query()
zamiast mysqli_query()
). Zezwolenie na wykonanie tylko jednego zapytania na raz jest bezpieczniejszym rozwiązaniem, ponieważ ogranicza możliwości atakującego.
Ochrona przed atakami SQL injection
Ochrona przed atakami SQL injection jest łatwa:
Filtruj dane.
Tego po prostu nie da się przecenić. Dzięki zastosowaniu dobrego filtra znika większość luk zabezpieczeń, a część zostaje praktycznie wyeliminowana.
Umieszczaj dane w cudzysłowach.
Jeśli baza danych na to pozwala (MySQL pozwala), wszystkie wartości w instrukcjach SQL umieszczaj w pojedynczych cudzysłowach, niezależnie od typu danych.
Stosuj symbole zastępcze w danych.
Czasami poprawne dane mogą przypadkowo kolidować z formatem instrukcji SQL. Użyj funkcji
mysql_escape_string()
lub innej właściwej dla bazy, której używasz. Jeśli taka nie istnieje, możesz w ostateczności poratować się funkcjąaddslashes()
.