PHP

Основы безопасного веб-программирования на 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. В штатном режиме работы сайта этот файл должен оставаться пустым, в идеале. Советую придерживаться этого при разработке сайта.
Теги:
hacking, PHP, безопасность
Добавлено: 27 Февраля 2015 22:59:20 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...