Основы безопасности PHP
Данный материал для начинающих программистов.
Содержание
- Демонстрация ошибок
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 быстрее, но совсем некрасиво. Красота — субъективное дело...)
<?php error_reporting(8191); if (!DEBUG) // Запись в БД или отсылка по почте вебмастеру. if ($errno == E_ERROR || $errno == E_PARSE || $errno == E_CORE_ERROR || $errno == E_COMPILE_ERROR || $errno == E_USER_ERROR) return true; --> set_error_handler("errorHandler"); --> ?>
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:
<?php ... if (check_admin($..., $...)) ... if ($user_level > 150) ?>
Если пользователь — не администратор, а переменная $user_level не инициализирована (ей не придано значение 0 в начале скрипта, в надежде, что оно 0 автоматически), то нехороший человек может дописать в адресной строке foo.php?user_level=999 и получить доступ.
SQL injection и magic_quotes
Так популярна среди начинающих конструкция
<?php $user = mysql_fetch_assoc(mysql_query("SELECT * FROM `users` WHERE `username` = "" AND `password` = """)); ?> опасна. Если пользователь введёт вместо пароля " OR `username` = "admin, то система впустит его как админа.
Приведённый пример, разумеется, элементарен. Но если не решить проблему глобально, всегда можно пропустить какой-нибудь запрос, подверженный SQL injection. Для борьбы с этим разработчики PHP решили сделать так, чтобы вся информация, поступающая от пользователя, подвергалась обработке и все кавычки escapeились (перед ними ставится слэш, что делает команда addslashes). Что случилось? Вся информация от пользователя приходит со слэшами. Даже та, что вроде слэши получить не должна. Например, комментарии к статье. Мало того, это не 100-процентный способ защиты от SQL injection.
Решение. а) со всей входящей информации снимаеи слэши, если они есть. б) Всю информацию, поступающую в SQL запрос фильтруем специально для этого созданной функцией mysql_real_escape_string (или аналогом для другой базы данных).
Снимаем слэши:
<?php function stripslashes_deep($value) $value = array_map("stripslashes_deep", $value); --> elseif (!empty($value) && is_string($value)) return $value; --> $_POST = stripslashes_deep($_POST); $_GET = stripslashes_deep($_GET); $_COOKIE = stripslashes_deep($_COOKIE); --> --> ?>
Создаём функцию для фильтрации (mysql_real_escape_string - длинно, да и привязанно к формату проверки. А если понадобиться поменять фильтр?)
<?php function quote($value) $value = """.mysql_real_escape_string($value)."""; --> return $value; --> ?>И используем её везде. Как только какие-то динамические данных отсылаются SQLу, сразу используем quote:
<?php $user = mysql_fetch_assoc(mysql_query("SELECT * FROM `users` WHERE `username` = ".quote($_POST["username"])." AND `password` = ".quote($_POST["password"]))); ?>
Проверка данных
Проверяйте всё, что вводит пользователь. По умолчанию он злоумышленник.
Старайтесь не фильтровать, старайтесь валидировать. Другими словами, не создавайте чёрного списка, создавайте белый. Вместо
<?php if (are_bad_symbols($data)) boo(); ?>используйте
<?php if (!all_good_symbols($data)) boo(); // Например: is_numeric($data); preg_match("/[a-z0-9_-]*/i", $data) ... ?> Так вы будете уверены, что информация чиста и никаких неожиданностей не будет. Если вы составляете список запрещённых символов, всегда можете просмотреть какой-нибудь нехороший %00 и подобные, о которых, скорее всего, не догадываетесь.
Разумеется, есть ситуации, когда нужна фильтрация, например, когда пользователь пишет комментарий. Тогда надо отсекать плохие символы. Но в принципе стараться надо валидировать.
Есть несколько команд, с которыми надо обращаться очень осторожно. Это include, require, readfile, eval, ``, system, exec, create_function, dir, fopen и подобные. Всегда посмотрите трижды, когда используете их, если в них используются данные, которые могут прийти от пользователя, будьте уверены — кто-то обязательно этим воспользуется.
<?php include($_GET["module"] . ".php"); ?>Этот кусок опасен. Если злоумышленник введёт "../../../../../etc/passwd%00", будет рад, а вы — вряд-ли.
Аутентификация
Не забывайте, что cookies редактируются ни чуть не сложнее, чем то, что видно в адресной строке. Поэтому всё, что приходит как печенье, потенциально — атака. Так что не надо хранить в cookies уровень доступа пользователя или его ID. Лучше всего дать PHP самому разбираться с этим, используя сессии.
<?php session_start(); $_SESSION["userid"] = 168; session_write_close(); ?>
Кстати, в cookies вообще хранить что-либо надо очень скромно и три раза подумать, а надо ли?
Вывод
Всё время думайте о данных в переменных $_GET, $_POST, $_COOKIE, как об атаке злоумышленника. Trust no one! :)
