Запрет межсайтовых сценариев (XSS) в ASP.NET Core

Автор: Рик Андерсон (Rick Anderson)

Межсайтовые скрипты (XSS) — это уязвимость системы безопасности, которая позволяет злоумышленнику размещать клиентские скрипты (обычно JavaScript) на веб-страницах. Когда другие пользователи загружают затронутые страницы, выполняются скрипты злоумышленника, позволяющие злоумышленнику украсть cookieмаркеры и маркеры сеанса, изменить содержимое веб-страницы с помощью DOM или перенаправить браузер на другую страницу. Уязвимости XSS обычно возникают, когда приложение принимает пользовательский ввод и выводит их на страницу без проверки, кодирования или экранирования.

Эта статья относится главным образом к ASP.NET Core MVC с представлениями, Razor Страницами и другими приложениями, возвращающими HTML-код, которые могут быть уязвимы для XSS. Веб-API, которые возвращают данные в формате HTML, XML или JSON, могут инициировать XSS-атаки в своих клиентских приложениях, если они не очищают введенные пользователем данные должным образом в зависимости от того, насколько доверяет клиентское приложение API. Например, если API принимает созданное пользователем содержимое и возвращает его в HTML-ответе, злоумышленник может внедрить вредоносные скрипты в содержимое, которое выполняется при отображении ответа в браузере пользователя.

Чтобы предотвратить XSS-атаки, веб-API должны реализовывать проверку входных данных и кодирование выходных данных. Проверка входных данных гарантирует, что введенные пользователем данные соответствуют ожидаемым критериям и не включают вредоносный код. Кодирование выходных данных обеспечивает правильную очистку всех данных, возвращаемых API, чтобы браузер пользователя не смог выполнить их в виде кода. Дополнительные сведения см. в этой статье об ошибке на GitHub.

Защита приложения от XSS

На базовом уровне XSS работает путем обмана приложения при вставке тега <script> на отображаемую On* страницу или путем вставки события в элемент. Чтобы избежать внедрения XSS в свои приложения, разработчикам следует выполнить следующие действия по предотвращению:

  1. Никогда не помещайте ненадежные данные во входные данные HTML, если вы не выполните остальные шаги ниже. Ненадежные данные — это любые данные, которые могут контролироваться злоумышленником, такие как входные данные HTML-формы, строки запросов, заголовки HTTP или даже данные, полученные из базы данных, так как злоумышленник может взломать базу данных, даже если не может нарушить безопасность приложения.

  2. Прежде чем помещать ненадежные данные в элемент HTML, убедитесь, что они закодированы в ФОРМАТЕ HTML. Кодирование HTML принимает такие символы, как < , и изменяет их в безопасную форму, например &lt;

  3. Прежде чем помещать ненадежные данные в атрибут HTML, убедитесь, что они закодированы в ФОРМАТЕ HTML. Кодирование атрибутов HTML — это надмножество кодировки HTML и кодирует дополнительные символы, такие как " и ".

  4. Прежде чем помещать ненадежные данные в JavaScript, поместите их в элемент HTML, содержимое которого извлекается во время выполнения. Если это невозможно, убедитесь, что данные закодированы в JavaScript. Кодирование JavaScript принимает опасные символы для JavaScript и заменяет их шестнадцатеричными символами, например, < будет закодировано как \u003C.

  5. Прежде чем помещать ненадежные данные в строку запроса URL-адреса, убедитесь, что они закодированы.

Кодирование HTML с помощью Razor

Подсистема, используемая Razor в MVC, автоматически кодирует все выходные данные, полученные из переменных, если вы не будете стараться предотвратить это. Он использует правила кодирования атрибутов HTML при каждом использовании директивы @ . Так как кодирование атрибутов HTML является надмножеством кодирования HTML, это означает, что вам не нужно беспокоиться о том, следует ли использовать кодировку HTML или кодирование атрибутов HTML. Необходимо убедиться, что @ используется только в контексте HTML, а не при попытке вставки ненадежных входных данных непосредственно в JavaScript. Вспомогательные функции тегов также кодируют входные данные, используемые в параметрах тегов.

Рассмотрим следующее Razor :

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

В этом представлении выводится содержимое переменной untrustedInput . Эта переменная содержит некоторые символы, которые используются в атаках XSS, а именно <, и >. При проверке источника отображаются отрисованные выходные данные, закодированные следующим образом:

&lt;&quot;123&quot;&gt;

Предупреждение

ASP.NET Core MVC предоставляет HtmlString класс, который не кодируется автоматически после вывода. Его никогда не следует использовать в сочетании с ненадежными входными данными, так как это приведет к уязвимости XSS.

Кодирование JavaScript с помощью Razor

Иногда может потребоваться вставить значение в JavaScript для обработки в представлении. Это можно сделать двумя способами. Самый безопасный способ вставки значений — поместить значение в атрибут данных тега и получить его в JavaScript. Пример:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it
    // can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Предыдущая разметка создает следующий КОД HTML:

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can
    // lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Приведенный выше код создает следующие выходные данные:

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

Предупреждение

НЕ сцепляйте ненадежные входные данные в JavaScript для создания элементов DOM или использования document.write() с динамически создаваемым содержимым.

Используйте один из следующих подходов, чтобы предотвратить предоставление кода XSS на основе DOM:

  • createElement() и присваивают значения свойств с помощью соответствующих методов или свойств, таких как node.textContent= или node.InnerText=.
  • document.CreateTextNode() и добавьте его в соответствующее расположение модели DOM.
  • element.SetAttribute()
  • element[attribute]=

Доступ к кодировщикам в коде

Кодировщики HTML, JavaScript и URL доступны в коде двумя способами:

  • Внедрение их с помощью внедрения зависимостей.
  • Используйте кодировщики по умолчанию, содержащиеся в System.Text.Encodings.Web пространстве имен .

При использовании кодировщиков по умолчанию любые настройки, примененные к диапазонам символов, которые считаются безопасными, не вступают в силу. Кодировщики по умолчанию используют наиболее безопасные правила кодирования.

Чтобы использовать настраиваемые кодировщики с помощью ВНЕДРЕНИЯ, конструкторы должны использовать параметры HtmlEncoder, JavaScriptEncoder и UrlEncoder соответствующим образом. Например:

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

Параметры URL-адреса кодирования

Если вы хотите создать строку запроса URL-адреса с ненадежными входными данными в UrlEncoder качестве значения, используйте для кодирования значения . Например,

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

После кодирования переменная encodedValue содержит %22Quoted%20Value%20with%20spaces%20and%20%26%22. Пробелы, кавычки, знаки препинания и другие небезопасные символы в процентах кодируются в шестнадцатеричном значении, например символ пробела станет %20.

Предупреждение

Не используйте ненадежные входные данные в составе URL-пути. Всегда передайте ненадежные входные данные в качестве значения строки запроса.

Настройка кодировщиков

По умолчанию кодировщики используют безопасный список, ограниченный базовым диапазоном Юникода для латинского языка, и кодируют все символы за пределами этого диапазона в качестве эквивалентов кода символов. Это поведение также влияет на Razor отрисовку TagHelper и HtmlHelper, так как использует кодировщики для вывода строк.

Причина этого заключается в защите от неизвестных или будущих ошибок браузера (предыдущие ошибки браузера споткнулся синтаксический анализ на основе обработки символов, отличных от английского). Если ваш веб-сайт интенсивно использует символы, отличные от латиницы, такие как китайский, кириллица или другие, это, вероятно, не то поведение, которое вы хотите.

Безопасные списки кодировщиков можно настроить так, чтобы они включали диапазоны Юникода, соответствующие приложению во время запуска, в Program.cs:

Например, использование конфигурации по умолчанию с помощью Razor HtmlHelper, аналогичного следующему:

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Предыдущая разметка отрисовывается с закодированным текстом на китайском языке:

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Чтобы расширение символов, которые кодировщик считал безопасными, вставьте следующую строку в Program.cs.

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

Вы можете настроить безопасные списки кодировщика так, чтобы они включали диапазоны Юникода, соответствующие вашему приложению во время запуска, в ConfigureServices().

Например, при использовании конфигурации по умолчанию можно использовать Razor HtmlHelper, как показано ниже.

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

При просмотре источника веб-страницы вы увидите, что она отрисовывается следующим образом с закодированным текстом на китайском языке;

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Для расширения символов, которые кодировщик рассматривает как безопасные, вставьте следующую строку в ConfigureServices() метод в startup.cs;

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

В этом примере расширяется список надежных, чтобы включить диапазон Юникода CjkUnifiedIdeographs. Отображаемые выходные данные теперь станут

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

Диапазоны безопасных списков указываются в виде диаграмм кода Юникода, а не языков. Стандарт Юникода содержит список диаграмм кода, которые можно использовать для поиска диаграммы, содержащей символы. Каждый кодировщик, Html, JavaScript и URL-адрес, должны быть настроены отдельно.

Примечание

Настройка списка надежных файлов влияет только на кодировщики, исходные с помощью внедрения зависимостей. Если вы напрямую обращаетесь к кодировщику через System.Text.Encodings.Web.*Encoder.Default , то по умолчанию будет использоваться список безопасных только для простого латинского языка.

Где должно происходить кодирование?

Общепринятая практика заключается в том, что кодирование происходит в точке вывода, и закодированные значения никогда не должны храниться в базе данных. Кодирование в точке вывода позволяет изменить использование данных, например, с HTML на значение строки запроса. Он также позволяет легко искать данные без необходимости кодирования значений перед поиском и позволяет воспользоваться всеми изменениями или исправлениями ошибок, внесенных в кодировщики.

Проверка как метод предотвращения XSS

Проверка может быть полезным инструментом для ограничения атак XSS. Например, числовая строка, содержащая только символы 0–9, не активирует XSS-атаку. Проверка усложняется при принятии HTML-кода в пользовательском вводе. Синтаксический анализ входных данных HTML является сложным, если не невозможным. Markdown в сочетании со средствами синтаксического анализа, которые удаляют внедренный HTML-код, является более безопасным вариантом для приема расширенных входных данных. Никогда не доверяйте только проверке. Всегда кодируйте недоверенные входные данные перед выводом, независимо от того, какая проверка или очистка была выполнена.