Данная статья не содержит никаких откровений. В первую очередь информация о типовых уязвимостях и методах их решения будет полезна начинающим. Опытные разработчики все это знают, или должны знать, если считают себя таковыми.
Большинство примеров кода не привязаны к какому-либо конкретному языку программирования, но для наглядности я буду использовать PHP.
Итак, поехали.
Допустим у вас есть вебсайт с формой ввода имени пользователя. Для проверки наличия имени в базе данных вы используте вот такой код:
где $_POST[«Username»] — введенное пользователем имя.
Пользователю достаточно ввести вот такое значение в поле Username
чтобы получился запрос, который всегда возвращает данные:
Для тех баз данных, которые поддерживают выполнение несколько запросов в одном пакете, злоумышленник может исполнить запрос удаляющий или изменяющий данные.
Пример такого ввода:
Запрос на выходе:
Два основных способа избежать SQL injection:
Наиболее надежный метод, но не всегда подходит. В PHP можно для этой цели использовать MySQLi
В PHP для этого есть функция mysql_real_escape_string, которая заменит опасные символы на escape последовательности. Наш пример теперь будет выглядеть вот так:
XSS уязвимости могут быть подвержены динамические вебсайты, где пользователи вводят какие-то свои данные, которые потом будут показаны на странице: форумы, гостевые книги, комментарии блогам и другое. Идея XSS заключается во встраивании в текст комментария какого-то Javascript кода, который исполнится, когда страницу откроет другой пользователь.
Чем может навредить безобидный джаваскрипт? Довольно многим, от открытие левых сайтов в попапе или простого завешивания браузера до воровства куки. Последнее уже может привести к выполнению другим пользователем действий на этом сайте от вашего имени. Рассмотрим минимальный пример.
Форма для ввода текста:
Файл showResults.php:
Мы видим, что введенный текст никак не обрабатывается и выводится на страницу в исходном виде. Теперь рассмотрим такой пример ввода:
Нетрудно видеть, что javascript код исполнится после сабмита формы. Лечится пропусканием ввода через htmlentities() непосредственно перед показом:
Здесь все очень просто. Если ваше приложение работает с финансовыми, медицинскими или просто с очень важными данными — используйте HTTPS. Данные между браузером и веб-сервером передаются в зашифрованном виде и не могут быть расшифрованы в случае перехвата сниффером.
Рассмотрим веб-приложение, где пользователи могут закачивать на сервер свои личные файлы. Некоторые файлы могут быть конфиденциальными и не должны быть доступны никому, кроме их владельца.
Есть закачивать все файлы в директорию вида public_html/files, то файл mysecretdoc.pdf будет доступен любому желающему по прямой ссылке mysecurewebsite.com/files/mysecretdoc.pdf.
Есть как минимум два способа предотвратить эту ситуацию:
— Не хранить пароли открытым текстом
Достаточно очевидное решение. Если мы будем хранить хэши паролей (MD5+salt), последстивия утечки таблицы паролей становятся намного менее серьезными, особенно в сочетании со следующим пунктом.
— Требовать, чтобы пароли удовлетворяли определенными правилам сложности и заставлять менять их через какое-то время. Как пользователь я не очень люблю этот метод, но он работает.
— Использовать комбинацию пароля (пин-кода) и устройства типа RSA токена для логина. Подойдет для банковских или внутрикорпоративных приложений.
— Сделать авторизацию через сторонний сервис, такой как Facebook, Twitter или OpenID. Пусть у них болит голова как уберечь пароли.
Здесь важно понимать, что шифрование и обфускация защищают от изменения и понимания кода, но не от его исполнения. Защифрованный код исполняется точно так же как и исходный и, более того, модуль расшифровки поставляется вместе с зашифрованным кодом и можеи быть использован для декодирования. Если злоумышленник получил доступ к вашему серверу, шифрование кода мало чем поможет.
Есть только один сценарий, когда это дело работает в качестве защиты от исполнения кода. Код расшифровки может устанавливаться как модуль на сервер и, если в руки злоумышленнику попал только сам зашифрованный код, без этого модуля он его не исполнит.
Шифрование кода для PHP: ionCube, ZendGuard, SourceGuardian
Обфускация: Thicket Obfuscator for PHP
Шифрование данных защищает от ситуации, когда база данных попала в чужие руки, но нет кода, который с ней работает.
Технически ничего сложного здесь нет. Шифрование/декодирование можно реализвать как средствами языка программирования так и в самой БД. Второй метод предпочтительнее, особенно если нужно реализовать поиск по зашифрованным данным.
Вот как это можно сделать для MySQL.
Шифрование с помощью триггеров
Декодирование в SQL запросе
Бонус для параноиков
В качестве эксперимента можно использовать пароль пользователя в качестве фразы шифрования данных. Если пользователей в системе несколько, персональные данные каждого из них шифруются своим собственным паролем. В таком случае даже получив доступ к коду или к базе данных злоумышленник не сможет расшифровать данные.
Из минусов:
— при смене пароля придется перешифровать данные
— в случае утери пароля восстановить данные не получится
В случае, когда ваше приложение хостится вместе с сотней других на одном сервере, кто-то может получить доступ ко всему серверу и прочитать или подделать переменные сессии.
Содержание типичного файла сессии PHP:
Решение:
— шифровать переменные сессии
— хранить данные сессии в БД. В PHP можно переопределить обработчик сессии с помощью функции session_set_save_handler
Как только система переводится в режим продакшен убедитесь что никакие необработанные сообщения об ошибках не будут показаны пользователю. Это может дать информацию о структуре базе данных или о структуре приложения.
Как минимум, сообщения об ошибках стоит отключить. В PHP это можно сделать вот так:
Наиболее же правильный метод это перехват сообщений об ошибке, запись их в БД, отправка уведомления разработчику итд. В PHP перехват сообщений об шибках делается с помощью функции set_error_handler(). И вот еще пример перехвата фатальных ошибок, которые нельзя перехватить с помощью set_error_handler().
Применимо к ситуации когда база данных расположена на другом сервере. Вот статья, которая рассказывает как создать SSL тоннель между MySQL и PHP.
Допустим у вас есть форма редактирования данных пользователя вот с таким УРЛ: example.com/edit_user.php?id=12345. Ничто не мешает пользователю 12345 поменять номер аккаунта в УРЛ и попытаться отредактировать другого пользователя. Простая проверка на стороне сервера пресекает эти попытки на корню.
Неискушенный прграммист может подумать, что заменив GET на POST мы избавимся от номеров аккаунтов в УРЛ и закроем уязвимость. Разумеется это не так. Сохранив страницу на свой компьютер и изменив данные формы, злоумышленник может подделать POST запрос.
Допустим у вас есть интернет магазин, в котором цена продукта берется из поля на форме. Поменяв значение поля, злоумышленник сможет купить товар по более низкой цене.
Эта тема плотно перекликается с валидацией всех данных, вводимых пользователем. Допустим у вас есть радио-кнопка с выбором пола.
Зная, что значение этого поля может быть только m или f, программист может посчитать проверку этого поля необязательным и записать его в базу данных в виде как оно есть.
Злоумышленник может сохранить эту страницу себе на диск и поменять ее следующим образом.
Даже если вы обработаете ввод с помощью mysql_real_escape_string(), значение все равно получится слишком длинным для односимвольного поля и поломает запрос (вот для чего мы советуем отключать сообщения об ошибках в продакшен версии).
Хорошим решением будет усечение этого поля до одного символа:
Эта уязвимость менее известна чем XSS, хотя не менее опасна. Представим себе форум, где участник Vasya постит сообщние, содержащее вот такой вот код:
Когда эту страницу откроет участник Petya, браузер исполнит запрос
Проблема в том, что если банк хранит данные доступа в куках, Petya будет залогинен автоматически и транзакция совершится от его имени, о чем он, конечно, и не подозревает.
Первая мысль которая приходит в голову — заметить GET на POST на всех важных формах (или вообще на всех). К сожалению это не решает проблему полностью. Ничто не мешает злоумышленнику разместить вот такую форму на своем вебсайте:
Если Васе удастся заманить Петю на свой вебсайт — форма будет отправлена и цель достигнута. Еще одна причина не ходить по подозрительным вебсайтам.
Как с этим бороться?
Дополнительная информация:
http://en.wikipedia.org/wiki/Cross-site_request_forgery
http://www.codinghorror.com/blog/2008/09/cross-site-request-forgeries-and-you.html
Теперь хорошие новости. От большинства этих уязвимостей несложно защититься. Многие PHP фреймворки (Yii, CakePHP, CodeIgniter, Zend, Symfony) и генераторы кода (PHPRunner) имеют встроенную защиту от большиства уязвимостей. Тем не менее, стоит понимать, как оно работает, чем чревато и как защититься. Предупрежден — значит вооружен.
Большинство примеров кода не привязаны к какому-либо конкретному языку программирования, но для наглядности я буду использовать PHP.
Итак, поехали.
1. Защита от SQL injection
Допустим у вас есть вебсайт с формой ввода имени пользователя. Для проверки наличия имени в базе данных вы используте вот такой код:
$query = "SELECT * FROM `Users` WHERE UserName='" . $_POST["Username"]. "'";
mysql_query($query);
где $_POST[«Username»] — введенное пользователем имя.
Пользователю достаточно ввести вот такое значение в поле Username
' or '1'='1
чтобы получился запрос, который всегда возвращает данные:
SELECT * FROM `Users` WHERE UserName = '' OR '1'='1'
Для тех баз данных, которые поддерживают выполнение несколько запросов в одном пакете, злоумышленник может исполнить запрос удаляющий или изменяющий данные.
Пример такого ввода:
a';DROP TABLE `Users`; SELECT * FROM `userinfo` WHERE 't' = 't
Запрос на выходе:
SELECT * FROM `Users` WHERE `UserName` = 'a';DROP TABLE `Users`; SELECT * FROM `userinfo` WHERE 't' = 't'
Два основных способа избежать SQL injection:
- параметризованные запросы
Наиболее надежный метод, но не всегда подходит. В PHP можно для этой цели использовать MySQLi
$stmt = $db->prepare('update people set name = ? where id = ?');
$stmt->bind_param('si',$name,$id);
$stmt->execute();
- escaping
В PHP для этого есть функция mysql_real_escape_string, которая заменит опасные символы на escape последовательности. Наш пример теперь будет выглядеть вот так:
$query = sprintf("SELECT * FROM `Users` WHERE UserName='%s'",
mysql_real_escape_string($_POST["Username"]));
mysql_query($query);
2. Защита от Сross Site Scripting (XSS)
XSS уязвимости могут быть подвержены динамические вебсайты, где пользователи вводят какие-то свои данные, которые потом будут показаны на странице: форумы, гостевые книги, комментарии блогам и другое. Идея XSS заключается во встраивании в текст комментария какого-то Javascript кода, который исполнится, когда страницу откроет другой пользователь.
Чем может навредить безобидный джаваскрипт? Довольно многим, от открытие левых сайтов в попапе или простого завешивания браузера до воровства куки. Последнее уже может привести к выполнению другим пользователем действий на этом сайте от вашего имени. Рассмотрим минимальный пример.
Форма для ввода текста:
<form id="myFrom" action="showResults.php" method="post">
<div><textarea name="myText" rows="4" cols="30"></textarea><br />
<input type="submit" value="Submit" name="submit" /></div>
</form>
Файл showResults.php:
echo("You typed this:");
echo($_POST['myText']);
Мы видим, что введенный текст никак не обрабатывается и выводится на страницу в исходном виде. Теперь рассмотрим такой пример ввода:
Нетрудно видеть, что javascript код исполнится после сабмита формы. Лечится пропусканием ввода через htmlentities() непосредственно перед показом:
echo("You typed this:");
echo(htmlentities($_POST['myText']));
3. Использование HTTPS
Здесь все очень просто. Если ваше приложение работает с финансовыми, медицинскими или просто с очень важными данными — используйте HTTPS. Данные между браузером и веб-сервером передаются в зашифрованном виде и не могут быть расшифрованы в случае перехвата сниффером.
4. Предотвращение скачивания пользовательских файлов по прямой ссылке
Рассмотрим веб-приложение, где пользователи могут закачивать на сервер свои личные файлы. Некоторые файлы могут быть конфиденциальными и не должны быть доступны никому, кроме их владельца.
Есть закачивать все файлы в директорию вида public_html/files, то файл mysecretdoc.pdf будет доступен любому желающему по прямой ссылке mysecurewebsite.com/files/mysecretdoc.pdf.
Есть как минимум два способа предотвратить эту ситуацию:
- вынеcти директорию files на уровень выше, чтобы она была вне корня вебсайта и не была доступна через веб
- использовать .htaccess для запрета прямого скачивания
5. Хранение паролей пользователей
— Не хранить пароли открытым текстом
Достаточно очевидное решение. Если мы будем хранить хэши паролей (MD5+salt), последстивия утечки таблицы паролей становятся намного менее серьезными, особенно в сочетании со следующим пунктом.
— Требовать, чтобы пароли удовлетворяли определенными правилам сложности и заставлять менять их через какое-то время. Как пользователь я не очень люблю этот метод, но он работает.
— Использовать комбинацию пароля (пин-кода) и устройства типа RSA токена для логина. Подойдет для банковских или внутрикорпоративных приложений.
— Сделать авторизацию через сторонний сервис, такой как Facebook, Twitter или OpenID. Пусть у них болит голова как уберечь пароли.
6. Шифрование и обфускация кода
Здесь важно понимать, что шифрование и обфускация защищают от изменения и понимания кода, но не от его исполнения. Защифрованный код исполняется точно так же как и исходный и, более того, модуль расшифровки поставляется вместе с зашифрованным кодом и можеи быть использован для декодирования. Если злоумышленник получил доступ к вашему серверу, шифрование кода мало чем поможет.
Есть только один сценарий, когда это дело работает в качестве защиты от исполнения кода. Код расшифровки может устанавливаться как модуль на сервер и, если в руки злоумышленнику попал только сам зашифрованный код, без этого модуля он его не исполнит.
Шифрование кода для PHP: ionCube, ZendGuard, SourceGuardian
Обфускация: Thicket Obfuscator for PHP
7. Шифрование данных
Шифрование данных защищает от ситуации, когда база данных попала в чужие руки, но нет кода, который с ней работает.
Технически ничего сложного здесь нет. Шифрование/декодирование можно реализвать как средствами языка программирования так и в самой БД. Второй метод предпочтительнее, особенно если нужно реализовать поиск по зашифрованным данным.
Вот как это можно сделать для MySQL.
Шифрование с помощью триггеров
delimiter |
CREATE TRIGGER insert_encrypt BEFORE INSERT ON cars
FOR EACH ROW BEGIN
SET NEW.Model = AES_ENCRYPT(NEW.Model,"my passphrase");
END;
|
delimiter |
CREATE TRIGGER update_encrypt BEFORE UPDATE ON cars
FOR EACH ROW BEGIN
SET NEW.Model = AES_ENCRYPT(NEW.Model,"my passphrase");
END;
|
Декодирование в SQL запросе
SELECT
...
AES_DECRYPT(Model,"my passphrase"),
...
FROM carscars
Бонус для параноиков
В качестве эксперимента можно использовать пароль пользователя в качестве фразы шифрования данных. Если пользователей в системе несколько, персональные данные каждого из них шифруются своим собственным паролем. В таком случае даже получив доступ к коду или к базе данных злоумышленник не сможет расшифровать данные.
Из минусов:
— при смене пароля придется перешифровать данные
— в случае утери пароля восстановить данные не получится
8. Защита данных сессии (PHP, shared server)
В случае, когда ваше приложение хостится вместе с сотней других на одном сервере, кто-то может получить доступ ко всему серверу и прочитать или подделать переменные сессии.
Содержание типичного файла сессии PHP:
userName|s:5:"admin";accountNumber|s:9:"123456789";
Решение:
— шифровать переменные сессии
— хранить данные сессии в БД. В PHP можно переопределить обработчик сессии с помощью функции session_set_save_handler
9. Обработка сообщений об ошибках
Как только система переводится в режим продакшен убедитесь что никакие необработанные сообщения об ошибках не будут показаны пользователю. Это может дать информацию о структуре базе данных или о структуре приложения.
Как минимум, сообщения об ошибках стоит отключить. В PHP это можно сделать вот так:
error_reporting(0);
@ini_set('display_errors', 0);
Наиболее же правильный метод это перехват сообщений об ошибке, запись их в БД, отправка уведомления разработчику итд. В PHP перехват сообщений об шибках делается с помощью функции set_error_handler(). И вот еще пример перехвата фатальных ошибок, которые нельзя перехватить с помощью set_error_handler().
10. Защита соединения между базой данных и приложением
Применимо к ситуации когда база данных расположена на другом сервере. Вот статья, которая рассказывает как создать SSL тоннель между MySQL и PHP.
11. Защита от form spoofing
Допустим у вас есть форма редактирования данных пользователя вот с таким УРЛ: example.com/edit_user.php?id=12345. Ничто не мешает пользователю 12345 поменять номер аккаунта в УРЛ и попытаться отредактировать другого пользователя. Простая проверка на стороне сервера пресекает эти попытки на корню.
Неискушенный прграммист может подумать, что заменив GET на POST мы избавимся от номеров аккаунтов в УРЛ и закроем уязвимость. Разумеется это не так. Сохранив страницу на свой компьютер и изменив данные формы, злоумышленник может подделать POST запрос.
Допустим у вас есть интернет магазин, в котором цена продукта берется из поля на форме. Поменяв значение поля, злоумышленник сможет купить товар по более низкой цене.
Эта тема плотно перекликается с валидацией всех данных, вводимых пользователем. Допустим у вас есть радио-кнопка с выбором пола.
<input name="gender" type="radio" value="m" />Male
<input name="gender" type="radio" value="f" />Female
Зная, что значение этого поля может быть только m или f, программист может посчитать проверку этого поля необязательным и записать его в базу данных в виде как оно есть.
Злоумышленник может сохранить эту страницу себе на диск и поменять ее следующим образом.
<input name="gender" type="text" value="m';DROP TABLE `Users`; ... " />
Даже если вы обработаете ввод с помощью mysql_real_escape_string(), значение все равно получится слишком длинным для односимвольного поля и поломает запрос (вот для чего мы советуем отключать сообщения об ошибках в продакшен версии).
Хорошим решением будет усечение этого поля до одного символа:
substr($_POST['gender'],0,1)
12. Защита от Cross-site request forgery (CSRF)
Эта уязвимость менее известна чем XSS, хотя не менее опасна. Представим себе форум, где участник Vasya постит сообщние, содержащее вот такой вот код:
<img src="http://mysecurebank.com/withdraw?account=petya&amount=1000000&for=vasya" />
Когда эту страницу откроет участник Petya, браузер исполнит запрос
http://mysecurebank.com/withdraw?account=petya&amount=1000000&for=vasya
Проблема в том, что если банк хранит данные доступа в куках, Petya будет залогинен автоматически и транзакция совершится от его имени, о чем он, конечно, и не подозревает.
Первая мысль которая приходит в голову — заметить GET на POST на всех важных формах (или вообще на всех). К сожалению это не решает проблему полностью. Ничто не мешает злоумышленнику разместить вот такую форму на своем вебсайте:
<form id="f" action="http://mysecurebank.com/withdraw" method="post">
<input name="account" value="petya" />
<input name="amount" value="1000000" />
<input name="for" value="vasya" />
</form>
Если Васе удастся заманить Петю на свой вебсайт — форма будет отправлена и цель достигнута. Еще одна причина не ходить по подозрительным вебсайтам.
Как с этим бороться?
- проверять значение HTTP реферера. Должно быть всегда с вашего собственного вебсайта. К сожалению положиться на это дело полностбю нельзя, так как многие прокси сервера могут его не передавать. К тому же его не так сложно подделать.
- использовать скрытое поле в форме с секретным значением, как правило привязанным к сессии пользователя. Злоумышленник не может прочитать форму от имени Пети, поэтому секретное значение окажется для него неизвестным (XmlHttpRequest не может выполнить запрос к другому серверу).
- Дополнительно отправлять куки через форму (прочитать с помощью джаваскрипта и вставить в форму). Если куки переданные через форму не совпадают с куками из заголовка — транзакцию не проводить.
- Ограничение времени жизни кук
Дополнительная информация:
http://en.wikipedia.org/wiki/Cross-site_request_forgery
http://www.codinghorror.com/blog/2008/09/cross-site-request-forgeries-and-you.html
Заключение
Теперь хорошие новости. От большинства этих уязвимостей несложно защититься. Многие PHP фреймворки (Yii, CakePHP, CodeIgniter, Zend, Symfony) и генераторы кода (PHPRunner) имеют встроенную защиту от большиства уязвимостей. Тем не менее, стоит понимать, как оно работает, чем чревато и как защититься. Предупрежден — значит вооружен.