Основы безопасного веб-программирования на 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="&quot; /&gt;&lt;em&gt;hacked!&lt;/em&gt;" />

В некоторых случаях можно пользоваться 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. В штатном режиме работы сайта этот файл должен оставаться пустым, в идеале. Советую придерживаться этого при разработке сайта.

Комментарии

Оставить комментарий »

 
qwe
19 сентября 2011, 19:21
#1
 

register_globals думаю тоже можно было бы упомянуть )

Joker-jar
20 сентября 2011, 0:14
#2
 

В последних версиях PHP оно по умолчанию отключено. Раньше, помню, зачем-то было включено )

Anhair
4 августа 2012, 20:44
#3
 

Для совместимости со старым быдло-кодом. Не все сайты на ПХП, которые содержат уязвимости ломают, т.к. далеко не все сайты смотрят в интернет. А директор и админ свято уверенны, что на просторах их локалки хакеров нет. И обычно безопасность таких сайтов или сильно страдает или вообще не задумывалась как класс.

vitalik-758153
27 июля 2013, 1:34
#4
 

Да

Оставить комментарий

Ваше имя
 
Ваш e-mail
 
Комментарий