Основы безопасности PHP
Начинающие (а часто и "опытные") программисты, пишущие на PHP склонны допускать очень похожие ошибки, которые могут привести к очень неприятным последствиям. Собственно, большая часть данной статьи не специфична для PHP и может относиться к любому языку, использующемуся для web-программирования.
Одним словом, всем, кто считает себя программистом, стоит это прочитать. Чтобы потом не рвать на себе волосы.
Содержание
Демонстрация ошибок ·
register_globals ·
SQL injection и magic_quotes ·
Валидация данных ·
Авторизация ·
Вывод ·
Демонстрация ошибок
Почему так часто я вижу, зайдя на какой-нибудь сайт что-то подобное этому:
Warning: Use of undefined constant LOCAL_SERVER - assumed 'LOCAL_SERVER' in /web/includes/page-definitions.php on line 13
Это одна из стандартных PHP ошибок, которая а) некрасива для пользователя; б) потенциально опасна. Поэтому их необходимо перехватывать и упорядочивать.
Во первых, функция error_reporting позволяет нам решить, какие ошибки мы хотим видеть. В принципе, достаточно просто выключить показ всех ошибок (error_reporting(0)), но нам нужно не это, потому что об ошибках мы хотим знать. Константа всех ошибок - E_ALL. В пятой версии появилась константа E_STRICT, показывающая строгие замечания по поводу кода. Разумеется, их желательно видеть, но они не входят в E_ALL, потому будем использовать числовое значение error_reporting(8191), которое вбирает всё, вплоть до новых ошибок шестой версии.
Примечание для любознательных: error_reporting(E_ALL | E_STRICT) не подходит, ибо тогда PHP 4 будет ругаться, не зная, что такое E_STRICT. С численным значением никаких проблем не будет.
Добавляем проверку на DEBUG - константу, выставленной в конфиге, и, с помощью set_error_handler, будем отлавливать ошибки в уже запущенном сервисе. Кстати, свой репортер ошибок должен возвращать true, иначе PHP выбросит стандартную ошибку.
Результат:
(Насчёт сравнения переменной с пятью параметрами я не уверен в выборе метода: in_array красивее, и гораздо медленнее, а switch case case быстрее, но совсем некрасиво. Красота - субъективное дело...)
-
if (!DEBUG) {
-
function errorHandler ($errno, $errstr, $errfile, $errline) {
-
// Запись в БД или отсылка по почте вебмастеру.
-
if ($errno == E_ERROR ||
-
$errno == E_PARSE ||
-
$errno == E_CORE_ERROR ||
-
$errno == E_COMPILE_ERROR ||
-
$errno == E_USER_ERROR) {
-
// Сообщение пользователю. Мол, «простите, облажались маленько»...
-
}
-
return true;
-
}
-
}
register_globals
До версии 4.2.0 директива register_globals была в PHP включена по умолчанию. Привело это к тому, что многие привыкли, что если в форме есть <input type="text" name="username" />, то в PHP коде можно проверять if ($username == 'admin') ...
Однако это потенциальная дыра, которая привела ко множеству взломов. Поэтому к POST, GET, COOKIE переменным надо обращаться через superglobals $_POST, $_GET, $_COOKIE. Многим это показалось слишком трудно и стала очень популярной команда import_request_variables, возвращающая всё на круги своя. Так вот. Не делайте этого.
Другая проблема с register_globals:
-
...
-
if (check_admin($..., $...)) {
-
...
-
$user_level = 169;
-
}
-
...
-
if ($user_level> 150) {
-
echo 'Boom!';
-
}
Если пользователь — не администратор, а переменная $user_level не инициализирована (ей не придано значение 0 в начале скрипта, в надежде, что оно 0 автоматически), то нехороший человек может дописать в адресной строке foo.php?user_level=999 и получить доступ.
SQL injection и magic_quotes
Так популярна среди начинающих конструкция
-
$user = mysql_fetch_assoc(mysql_query("SELECT * FROM `users` WHERE `username` = '{$_POST['username'}' AND `password` = '{$_POST['password']}'"));
опасна. Если пользователь введёт вместо пароля ' OR `username` = 'admin, то система впустит его как админа.
Приведённый пример, разумеется, элементарен. Но если не решить проблему глобально, всегда можно пропустить какой-нибудь запрос, подверженный SQL injection. Для борьбы с этим разработчики PHP решили сделать так, чтобы вся информация, поступающая от пользователя, подвергалась обработке и все кавычки escapeились (перед ними ставится слэш, что делает команда addslashes). Что случилось? Вся информация от пользователя приходит со слэшами. Даже та, что вроде слэши получить не должна. Например, комментарии к статье. Мало того, это не 100-процентный способ защиты от SQL injection.
Решение. а) со всей входящей информации снимаем слэши, если они есть. б) Всю информацию, поступающую в SQL запрос фильтруем специально для этого созданной функцией mysql_real_escape_string (или аналогом для другой базы данных).
Снимаем слэши:
-
function stripslashes_deep($value) {
-
}
-
}
-
return $value;
-
}
-
-
$_POST = stripslashes_deep($_POST);
-
$_GET = stripslashes_deep($_GET);
-
$_COOKIE = stripslashes_deep($_COOKIE);
-
}
Создаём функцию для фильтрации (mysql_real_escape_string - длинно, да и привязанно к формату проверки. А если понадобиться поменять фильтр?)
-
function quote($value) {
-
}
-
return $value;
-
}
И используем её везде. Как только какие-то динамические данных отсылаются SQLу, сразу используем quote:
-
$user = mysql_fetch_assoc(mysql_query('SELECT * FROM `users` WHERE `username` = '.quote($_POST['username']).' AND `password` = '.quote($_POST['password'])));
Валидация данных
Валидируйте всё, что вводит пользователь. По умолчанию он злоумышленник.
Не проверяйте на наличие плохих символов. Проверяйте на отсутствие нехороших. Вместо
-
if (are_bad_symbols($data)) boo();
используйте
-
if (!all_good_symbols($data)) boo();
-
// Например:
-
...
Так вы будете уверены, что информация чиста и никаких неожиданностей не будет. Если вы составляете список запрещённых символов, всегда можете просмотреть какой-нибудь нехороший %00 и подобные, о которых, скорее всего, не догадываетесь.
Есть несколько команд, с которыми надо обращаться очень осторожно. Это include, require, readfile, eval, ``, system, exec, create_function, dir, fopen и подобные. Всегда посмотрите дважды, когда используете их, если в них используются данные, которые могут прийти из $_GET, $_POST, $_COOKIE, будьте уверены - кто-то обязательно этим воспользуется.
-
include($_GET['module'] . '.php');
Этот кусок опасен. Если злоумышленник введёт '../../../../../etc/passwd%00', будет рад, а вы - вряд-ли.
Авторизация
Не забывайте, что cookies редактируются ни чуть не сложнее, чем то, что видно в адресной строке. Поэтому всё, что приходит как печенье, потенциально - атака. Так что не надо хранить в cookies уровень доступа пользователя или его ID. Лучше всего дать PHP самому разбираться с этим, используя сессии.
-
$_SESSION['userid'] = 168;
Кстати, в cookies вообще хранить что-либо надо очень скромно и три раза подумать, а надо ли?
Вывод
Всё время думайте о данных в переменных $_GET, $_POST, $_COOKIE, как об атаке злоумышленника. Фильтрация, проверка, недоверие!
Источник статьи: Хабрахабр

