Основы безопасного веб-программирования на PHP
Основными критериями добротного веб-приложения являются, безусловно, его функциональность и качество реализации. Но зачастую многие забывают о таком важном критерии как безопасность. В особенности это касается начинающих веб-программистов. Мы то и дело слышим новости об очередном взломе какого-то сайта. В силу своих особенностей реализации веб-приложения имеют очень много потенциальных путей обхода предусмотренных ограничений. Однако, соблюдая несколько простых принципов веб-программирования можно свести риск допущения уязвимости на сайте до минимума. О них речь и пойдет в этой статье.
Узкими местами веб-приложения являются места обработки приходящих извне данных. Давайте подумаем, какие данные скрипт может получать из внешних по отношению к себе источников:
- GET и POST-данные. Конечно же, данные передаваемые пользователем в виде GET и POST-параметров. Это самый распространенный источник внешних данных, соответственно, самая большая доля уязвимостей приходится на него;
- Куки. Те же пользовательские данные, другой метод их передачи;
- Данные из БД. Запрашиваемые скриптом данные из базы данных;
- Внешний контент. Внешний контент, который скрипт загружает в процессе работы. Например, сторонняя RSS-лента новостей и тому подобное.
По большому счету, источник данных не является определяющим фактором. Гораздо важнее правильно обработать эти данные в соответствии с их дальнейшим назначением.
База данных
Рассмотрим случай, когда пришедшие извне данные используются в операциях с базой данных. Предположим, что на сайте имеется HTML-форма авторизации с параметрами user и password и таким обработчиком:
$user = $_POST['user']; $password = $_POST['password']; $sql = "SELECT Count(*) FROM users WHERE user = '$user' AND password = '$password' LIMIT 1"; //--Выполнение запроса и анализ результата
Все предельно просто, но настораживает отсутствие какой-либо обработки внешних данных. Так, злоумышленник может в качестве имени передать следующую конструкцию:
admin'/*
и пустой пароль. В результате обработчик сформирует SQL-запрос:
SELECT COUNT(*) FROM users WHERE USER = 'admin'/*' AND password = '' LIMIT 1
(последовательность /* в SQL считается началом комментария, текст после него не учитывается) который авторизует его под администраторским аккаунтом без указания пароля. Данная уязвимость называется SQL-инъекция. Почему это произошло? Еще раз внимательно посмотрите на конструкцию. В ней передается символ ', который преждевременно закрывает строковый параметр в SQL-запросе, что дает возможность изменять конструкцию запроса. Чтобы избежать этого необходимо экранировать кавычки в строковых данных, которые используются в SQL-запросах. При экранировании в строке перед каждым управляющим символом ставится символ \, который подавляет управляющие функции символа. Для экранирования строковых данных, используемых для работы с MySQL используйте функцию mysql_real_escape_string. Модифицированный обработчик выглядит следующим образом:
$user = mysql_real_escape_string( $_POST['user'] ); $password = mysql_real_escape_string( $_POST['password'] ); $sql = "SELECT Count(*) FROM users WHERE user = '$user' AND password = '$password' LIMIT 1"; //--Выполнение запроса и анализ результата
В этом случае злоумышленник не сможет специальным образом сформированными данными изменить структуру SQL-запроса, т.к. внешние строковые параметры гарантированно будут обрамлены кавычками.
Нужно быть не менее бдительным с работой с числовыми данными. Предположим, что на сайте существуют URL вида product.php?id=[число]. В скрипте product.php данные о продукте из базы данных получаются следующим образом:
$id = $_GET['id']; $sql = "SELECT * FROM products WHERE id = $id LIMIT 1"; //--Выполнение запроса и анализ результата
И снова уязвимость. id никоим образом не обрабатывается, поэтому вместо числового идентификатора можно передать часть SQL-кода, который передастся на выполнение MySQL-серверу. Экранирование в данном случае не поможет, так как параметр в SQL-запросе не обрамлен кавычками (кстати, числовые значения можно также брать в кавычки для повышения надежности). Поэтому последуем следующему правилу: число должно быть обязательно числом в SQL-запросе. id во всех штатных ситуациях всегда будет числом, и только при вмешательстве злоумышленника там могут появиться какие-то строковые данные. Поэтому принудительно приводим входящий параметр id к числовому типу. Для этого в PHP существуют функция intval:
$id = intval( $_GET['id'] ); $sql = "SELECT * FROM products WHERE id = $id LIMIT 1"; //--Выполнение запроса и анализ результата
Пример работы функции intval (вход — выход):
3 — 3 126 — 126 43hack — 43 hacked — 0
Контент
Следующий случай — когда внешние данные используются для формирования контента страницы. Основная потенциальная проблема в этом случае — нежелательный HTML-код. Рассмотрим пример. На сайте имеется скрипт поиска search.php со следующим кодом:
//--Если передан парам. query, используем его, иначе пустую строку $search = isset($_GET['query']) ? $_GET['query'] : ''; ... echo '<input type="text" name="query" value="' . $search . '" />'; ...
То есть имеется форма с текстовым полем для ввода поисковой фразы. При поиске (GET-запросе) поисковая фраза пользователя остается в поле. Все работает нормально пока в поле вводится обычный текст. Но если ввести:
" /><em>hacked!</em>
то после подстановки значения в поле получаем:
<input type="text" name="query" value="" /><em>hacked!</em>" />
Злоумышленником был закрыт HTML-тег input и на страницу попал HTML-код. Такая уязвимость позволяет организовать пассивную XSS-атаку на сайт. Предотвратить это просто, достаточно фильтровать HTML-код там, где он не нужен. Для этого я рекомендую использовать функцию htmlspecialchars, которая преобразовывает управляющие HTML-символы в безопасные последовательности. В результате браузером HTML-код не интерпретируется, а отображается как есть. Использование:
//--Если передан парам. query, используем его, иначе пустую строку $search = isset($_GET['query']) ? $_GET['query'] : ''; ... echo '<input type="text" name="query" value="' . htmlspecialchars($search) . '" />'; ...
Теперь в случае ввода атакующего кода сформированное поле будет выглядеть так:
<input type="text" name="query" value="" /><em>hacked!</em>" />
В некоторых случаях можно пользоваться PHP-функцией strip_tags, которая полностью удаляет HTML-теги из текста.
Файловая система
Иногда внешние данные используются для доступа к файловой системе. Допустим, на сайте существуют URL index.php?page=board, index.php?page=glory, index.php?page=contacts, в скрипте index.php обрабатывается это следующим образом:
$page = $_GET['page']; echo file_get_contents($page . '.txt');
Такой код уязвим. Злоумышленник может в параметре передать путь к постороннему файлу и увидеть его содержимое. Допустим, конфигурационного файла:
/index.php?page=../config.php%00
Вообще, я не рекомендую подобный подход, гораздо безопаснее реализовать такое же примерно так:
$page = $_GET['page']; switch ($page) { case 'board' : echo file_get_contents('board.txt'); break; case 'glory' : echo file_get_contents('glory.txt'); break; case 'contacts' : echo file_get_contents('contacts.txt'); break; default : ... ; }
Но если все же необходимо использовать внешний параметр как имя какого-то файла, то обязательно его фильтруем. Для этого можно воспользоваться функцией basename, которая, при наличии в строке пути к файлу и расширения, обрежет их, оставив только имя:
/etc/passwd — passwd /home/joker/test.txt — test
Общие рекомендации
Помимо конкретных приемов, часть которых мы рассмотрели выше, существуют и общие рекомендации, которые повышают надежность сайта. К таким можно отнести отключение вывода PHP-ошибок. В рабочей версии сайта они совершенно ни к чему, а злоумышленнику будет намного сложнее вслепую искать уязвимости, если даже таковые существуют. Отключить вывод ошибок можно параметром display_errors в конфигурационном файле PHP. Это также можно сделать в файле .htaccess:
php_flag display_errors off
или даже в самом PHP-скрипте:
ini_set('display_errors', 0);
При этом желательно включить логирование ошибок параметром log_errors, параметру error_reporting выставить значение E_ALL и регулярно просматривать error.log. В штатном режиме работы сайта этот файл должен оставаться пустым, в идеале. Советую придерживаться этого при разработке сайта.
register_globals думаю тоже можно было бы упомянуть )
В последних версиях PHP оно по умолчанию отключено. Раньше, помню, зачем-то было включено )
Для совместимости со старым быдло-кодом. Не все сайты на ПХП, которые содержат уязвимости ломают, т.к. далеко не все сайты смотрят в интернет. А директор и админ свято уверенны, что на просторах их локалки хакеров нет. И обычно безопасность таких сайтов или сильно страдает или вообще не задумывалась как класс.
Да