Безопасность ASP.NET

Защита ваших приложений ASP.NET

Эдам Тьюлипер

В этой статье обсуждается бета-версия Microsoft Anti-Cross Site Scripting Library (AntiXSS).

Продукты и технологии:

ASP.NET, SQL Server, Microsoft Anti-Cross Site Scripting Library

В статье рассматриваются:

  • кросс-сайтовые скрипты;
  • поддержка кросс-сайтовых запросов.

В предыдущем номере я рассказал, как важно встраивать средства защиты в веб-приложения, и рассмотрел некоторые типы атак, в том числе со встраиванием SQL-кода и модификацией параметров, а также пояснил, как предотвратить их (msdn.microsoft.com/magazine/hh580736). В этой статье мы детально обсудим еще две распространенные атаки: с использованием кросс-сайтовых скриптов (cross-site scripting, XSS) и подделкой кросс-сайтовых запросов (cross-site request forgery, CSRF).

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

Кросс-сайтовые скрипты

Что это такое? XSS — это атака, при которой в пользовательский сеанс просмотра сети вводится злонамеренный скрипт, и, как правило, пользователь об этом ничего не знает. Такие типы атак стали достаточно известны многим людям из-за инцидента, который произошел в одной крупной социальной сети. Она подверглась XSS-атаке, и от имени ее пользователей стали публиковаться сообщения без их ведома. Если атакующий отправляет злонамеренный скрипт и ему удается заставить браузер выполнить его, этот скрипт работает в контексте сеанса жертвы, фактически позволяя атакующему делать с DOM все, что угодно, в том числе выводить фальшивые диалоги входа или красть файлы cookie. При такой атаке возможна даже установка HTML-перехватчика нажатий клавиш (key logger) в текущей странице для постоянной передачи ввода из этого окна на удаленный сайт.

Результат атаки XSS используется несколькими способами, каждый из которых опирается на вывод без escape-символов или с некорректными escape-символами.

Как эксплуатируется результат этой атаки? Результат атаки XSS используется несколькими способами, каждый из которых опирается на вывод без escape-символов или с некорректными escape-символами. Давайте рассмотрим случай с приложением, которому нужно отображать пользователю простое сообщение о состоянии. Обычно это сообщение передается в строке запроса, как показано на рис. 1.

Query String Message

Рис. 1. Сообщение со строкой запроса

Этот прием часто используется после перенаправления (redirect), чтобы показать пользователю какое-то сообщение о состоянии, например сообщение Profile Saved на рис. 1. Сообщение считывается из строки запроса и записывается прямо на странице. Если вывод не кодируется в HTML, любой может легко встроить JavaScript-код вместо сообщения о состоянии. Этот вариант называют отраженной XSS-атакой (reflected XSS attack), так как при этом осуществляется рендеринг любого содержимого строки запроса непосредственно в странице. Злонамеренный скрипт в случае постоянной атаки сохраняется обычно в базе данных или в файле cookie.

Здесь эксплуатируется главным образом то, что результаты кодируются не по стандарту HTML.

На рис. 1 видно, что этот URI принимает параметр msg. Веб-страница для этого URI содержала бы, например, следующий код для простой записи этой переменной в страницу без кодирования:

<div class="messages"> <%=Request.QueryString["msg"]%></div>

Если заменить «Profile Saved» скриптом, показанным на рис. 2, в браузере будет показана функция alert из скрипта, включенного в строку запроса. Здесь эксплуатируется главным образом то, что результаты не кодируются в HTML, поэтому тег <script> на самом деле разбирается браузером как корректный JavaScript-код, а потом выполняется. Очевидно, это вовсе не то, что задумывал разработчик. Injecting Script into the URI

Рис. 2. Встраивание скрипта в URI

Итак, на экране всплывет оповещение alert — ну и что? Давайте немного изменим этот пример, как показано на рис. 3. Заметьте, что здесь я сократил атакующий скрипт, чтобы не затемнять общую картину; этот синтаксис не совсем правилен, но небольшая модификация превратила бы его в реальный атакующий скрипт.

Creating a Malicious Attack

Рис. 3. Создание атакующего скрипта

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

Рис. 4 иллюстрирует атаку, использующую схожий скрипт на сайте разработки, на котором пользователям разрешается оставлять комментарии о продуктах. Однако вместо настоящей рецензии на продукт некто вводит злонамеренный JavaScript-код как комментарий. Этот скрипт теперь отображает диалог входа для каждого пользователя, попадающего на эту веб-страницу, собирает введенные удостоверения и отсылает их на какой-то удаленный сайт. Это постоянная XSS-атака (persistent XSS attack); скрипт сохраняется в базе данных и выполняется для каждого, кто заходит на эту страницу.

A Persistent XSS Attack Showing a Fake Dialog

Рис. 4. Постоянная XSS-атака, обеспечивающая отображение фальшивого диалога

Другой способ эксплуатации XSS — использование HTML-элементов, когда в HTML-тегах разрешается наличие динамического текста, например:

<img onmouseover=alert([user supplied text])>

Если атакующий вставляет текст вроде «onmouseout=alert(docu­ment.cookie)», это создает следующий тег в браузере, когда он обращается к файлу cookie:

<img onmouseover=alert(1) onmouseout=alert(document.cookie) >

Тега <script>, потенциально способного фильтровать ввод, здесь нет и нечего комбинировать со знаком перехода (escape), но это совершенно корректный блок JavaScript-кода, который может считывать файл cookie — причем потенциально это может быть аутентификационный cookie. Есть специфические способы делать это безопаснее, но из-за риска лучше не допускать того, чтобы пользовательский ввод попадал в этот подставляемый в строку код.

Как предотвратить XSS? Если вы будете строго следовать перечисленным ниже правилам, то сможете предотвратить большинство (или даже все) XSS-атаки на свое приложение.

  1. Убедитесь, что весь ваш вывод кодируется в HTML.

  2. Не допускайте появления передаваемого пользователем текста в строке атрибута любого HTML-элемента.

  3. Не допускайте использования своим приложением браузера Internet Explorer 6, проверяя Request.Browser, как описано в msdn.microsoft.com/library/3yekbd5b.

  4. Четко представляйте поведение своего элемента управления и выясните, кодирует ли он свой вывод в HTML. Если нет, кодируйте данные, поступающие в этот элемент управления.

  5. Используйте Microsoft Anti-Cross Site Scripting Library (AntiXSS) и задайте ее как свой кодировщик в стандарте HTML по умолчанию.

  6. С помощью объекта AntiXSS Sanitizer (эта библиотека скачивается отдельно, и я расскажу о ней позже) вызывайте GetSafeHtml или GetSafeHtmlFragment до сохранения HTML-данных в базе данных; не кодируйте данные перед сохранением.

  7. В случае Web Forms не используйте на своих веб-страницах присваивание EnableRequestValidation=false. К сожалению, в большинстве сообщений на форумах советуют отключать этот параметр, если возникает какая-то ошибка. Этот параметр существует не просто так и блокирует запрос, если на сервер выполняется обратная передача определенной комбинации символов, например «<X». Если ваши элементы управления возвращают серверу HTML-код и в ответ получают ошибку, как показано на рис. 5, вы должны в идеале кодировать данные, прежде чем возвращать их на сервер. Это распространенная ситуация в случае элементов управления WYSIWYG, и большинство их современных версий будет корректно кодировать свои HTML-данные перед обратной передачей на сервер.

    Server Error from Unencoded HTML

    Рис. 5. Серверная ошибка из-за незакодированного HTML

  8. В случае приложений ASP.NET MVC 3 не используйте ValidateInput(false) для отключения Request Validation (проверки запросов), когда вам нужно передать HTML обратно в свою модель. Просто добавьте [AllowHtml] к свойству модели, например:

public class BlogEntry
{
  public int UserId {get;set;}
  [AllowHtml]
  public string BlogText {get;set;}
}

Некоторые продукты для распознавания XSS-атак пытаются обнаруживать <script> и другие комбинации слов или шаблоны на основе регулярных выражений в строке. Эти продукты могут поддерживать дополнительные проверки, но полностью полагаться на них нельзя из-за большого количества вариаций, которые могут создаваться атакующими. Взгляните на XSS Cheat Sheet на сайте ha.ckers.org/xss.html, чтобы получить представление о том, насколько сложным может оказаться распознавание.

Чтобы понять меры защиты, допустим, что атакующий встроил некий скрипт, который появляется в какой-то переменной в нашем приложении либо через строку запроса, либо от поля на форме:

string message = Request.QueryString["msg"];

или:

string message = txtMessage.Text;

Заметьте: хотя элемент управления TextBox кодирует в HTML свой вывод, он не делает этого для свойства Text, когда вы считываете его из кода. При наличии любой из двух показанных выше строк кода вы остаетесь со следующей строкой в переменной message:

message = "<script>alert('bip')</script>"

На веб-странице, содержащей код, аналогичный следующему, в браузере пользователя будет выполняться JavaScript-код просто потому, что этот текст был записан в страницу:

<%=message %>

Кодирование вывода в HTML убивает эту атаку в зародыше. В табл. 1 перечислены основные варианты кодирования опасных данных.

Табл. 1. Варианты кодирования в HTML

ASP.NET (либо MVC, либо Web Forms) <%=Server.HtmlEncode(message) %>
Web Forms (синтаксис ASP.NET 4) <%: message %>
ASP.NET MVC 3 Razor @message
Связывание с данными

К сожалению, в синтаксис связывания с данными пока не встроен синтаксис кодирования; такой синтаксис появится в следующей версии ASP.NET в форме <%#: %>. А пока используйте:

<%# Server.HtmlEncode(Eval("PropertyName")) %>

Более эффективное кодирование

Из AntiXSS Library в пространстве имен Microsoft.Security.Application:

Encoder.HtmlEncode(message)

Эти варианты предотвращают разновидность атаки, показанной в примере, и должны использоваться в ваших приложениях.

AntiXSS Library подверглась весьма основательной переработке.

Важно знать свои элементы управления. Какие из них кодируют данные в HTML за вас, а какие — нет? Например, TextBox кодирует в HTML визуализируемый вывод, но LiteralControl этого не делает. Это важное различие. Если текстовому полю присвоить

yourTextBoxControl.Text = "Test <script>alert('bip')</script>";

то он выполняет корректный рендеринг текст для страницы:

Test &lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;

Противоположный пример:

yourLiteralControl.Text = "Test <script>alert('bip')</script>";

Здесь на странице будет отображена JavaScript-функция alert, подтверждая уязвимость перед XSS-атаками. Устранить эту уязвимость довольно легко:

yourLiteralControl.Text = Server.HtmlEncode(
    "Test <script>alert('bip')</script>");

При использовании связывания с данными в Web Forms устранить уязвимость немного труднее. Посмотрите на следующий пример:

<asp:Repeater ID="Repeater1" runat="server">
  <ItemTemplate>
    <asp:TextBox ID="txtYourField"
      Text='<%# Bind("YourField") %>'
      runat="server"></asp:TextBox>
  </ItemTemplate>
</asp:Repeater>

Он уязвим? Нет. Хотя кажется, что подставляемый в строку код мог бы записать скрипт или вырваться за границы кавычек, он на самом деле кодируется.

А как насчет этого:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%# Eval("YourField") %>
  </ItemTemplate>
</asp:Repeater>

Он уязвим? Да. Синтаксис связывания с данными <%# %> не обеспечивает кодирования в HTML. Вот исправление:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Eval("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

Имейте в виду: если вы используете Bind в этом сценарии, вы не сможете обернуть его в Server.HtmlEncode из-за того, что «за кулисами» Bind компилируется как два раздельных вызова. Этот блок кода неправильный:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Bind("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

Если вы используете Bind и не присваиваете текст элементу управления, который кодирует его в HTML (например, элементу управления TextBox), подумайте о его замене на Eval, чтобы вызов можно было обернуть в Server.HtmlEncode, как в предыдущем примере.

Аналогичная ситуация со связыванием с данными и в ASP.NET MVC, где вы должны знать, кодируют ли текст вспомогательные HTML-методы (helpers). Такие методы для меток и текстовых полей выполняют кодирование в HTML. Так, следующий код:

@Html.TextBox("customerName", "<script>alert('bip')</script>")
@Html.Label("<script>alert('bip')</script>")

превращается в:

<input id="customerName" name="customerName" type="text"
  value="&lt;script>alert(&#39;bip&#39;)&lt;/script>" />
<label for="">&lt;script&gt;alert(&#39;bip&#39;)&lt;
  /script&gt;</label>

Ранее я упоминал об AntiXSS. Текущая версия — 4.1 beta 1. AntiXSS Library подверглась весьма основательной переработке, и, что касается защиты, предоставляет более эффективный HTML-кодировщик, чем тот, который входит в состав ASP.NET. Не то чтобы с Server.HtmlEncode что-то не в порядке, но его основное предназначение — обеспечение совместимости, а не безопасности. В AntiXSS применяется другой подход к кодированию. Узнать больше можно по ссылке msdn.microsoft.com/security/aa973814.

Эта бета-версия доступна по ссылке bit.ly/gMcB5K. Пока AntiXSS не вышла за рамки бета-версии, скачивайте код и компилируйте его. Джон Гэллоуэй (Jon Galloway) опубликовал отличную статью на эту тему (bit.ly/lGpKWX).

Чтобы задействовать кодировщик AntiXSS, просто поставьте такой вызов:

<%@ Import Namespace="Microsoft.Security.Application" %>
...
<%= Encoder.HtmlEncode(plainText)%>

В ASP.NET MVC 4 добавлена отличная новая функциональность, которая позволяет переопределять исходный ASP HTML-кодировщик и заменять его на кодировщик из AntiXSS. На момент написания этой статьи, вам нужна версия 4.1; поскольку сейчас это бета-версия, вы должны скачать код, скомпилировать его и добавить библиотеку к своему приложению как ссылку — на все это уходит пять минут. Затем добавьте в свой web.config в раздел <system.web> такую строку:

<httpRuntime encoderType="Microsoft.Security.Application.
  AntiXssEncoder, AntiXssLibrary"/>

Теперь любой HTML-кодированный вызов, инициированный с использованием любого синтаксиса, перечисленного в табл. 1, в том числе синтаксиса ASP.NET MVC 3 Razor, будет кодироваться библиотекой AntiXSS. Неплохо для подключаемой функциональности?

Эта библиотека также включает объект Sanitizer, который можно использовать для чистки HTML перед его сохранением в базе данных, что очень удобно, если вы предоставляете WYSIWYG-редактор, с помощью которого пользователь может редактировать HTML. Этот вызов пытается удалить скрипт из строки:

using Microsoft.Security.Application;
...
string wysiwygData =
  "before <script>alert('bip ')</script> after ";
string cleanData = Sanitizer.GetSafeHtmlFragment(wysiwygData);

В итоге вы получаете следующую очищенную строку, которая потом сохраняется в базе данных:

cleanData = "before  after ";

В ASP.NET MVC 4 добавлена отличная новая функциональность, которая позволяет переопределять исходный ASP HTML-кодировщик.

Подделка кросс-сайтовых запросов

Что это такое? Подделка кросс-сайтовых запросов (cross-site request forgery, CSRF) (произносится как «sea-surf») — атака, при которой некто использует отношения доверия между вашим браузером и неким веб-сайтом для выполнения команды в сеансе невинного пользователя. Эта атака немного сложнее в понимании без знания деталей, поэтому перейдем сразу к ним.

Как эксплуатируется результат этой атаки? Допустим, Джон аутентифицируется как администратор на сайте PureShoppingHeaven. Этот сайт имеет URL, доступный только администратору, и на него можно передавать информацию для выполнения какой-либо операции, например создания нового пользователя, как показано на рис. 6.

Passing Information on the URL
Рис. 6. Передача информации на URL

Если атакующий сможет одним из множества методов заставить Джона запросить этот URL, его браузер запросит данный URL от сервера и отправит на него любую аутентификационную информацию, которая может быть кеширована браузером Джона или использоваться им, например аутентификационные cookie или другие аутентификационные маркеры, в том числе от Windows Authentication.

Это простой пример, но CSRF-атаки могут быть куда более изощренными и могут включать команды POST для форм в дополнение к запросам GET, а также одновременно использовать другие атаки вроде XSS.

Так задумано, что браузер будет отправлять любые корректные cookie или аутентификационную информацию.

Предположим, Джон посещает уязвимую социальную сеть, сайт которой эксплуатируется злоумышленниками. Возможно, атакующий разместил немного JavaScript-кода на странице, задействовав уязвимость к XSS-атаке, и теперь она запрашивает URL-адрес AddUser.aspx в сеансе Джона. Следующий дамп утилиты Fiddler (fiddler2.com) после посещения Джоном той веб-страницы показывает, что браузер также передал модифицированный аутентификационный cookie (custom site-authentication cookie):

GET http://pureshoppingheaven/AddUser.aspx?userName=
  hacked&pwd=secret HTTP/1.1
Host: pureshoppingheaven
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0;
  Windows NT 6.1; Trident/5.0)
Cookie: CUSTOMERAUTHCOOKIE=a465bc0b-e1e2-4052-8292-484d884229ab

Все это происходит без ведома Джона. Важно понимать: так задумано, что браузер будет отправлять любые корректные cookie или аутентификационную информацию. Вы когда-нибудь замечали, что ваш почтовый клиент, как правило, не загружает изображения по умолчанию? Одна из причин этого — предотвращение CSRF. Если вы получаете HTML-сообщение электронной почты со встроенным тегом image, как показано ниже, то данный URL будет запрошен и сервер выполнит соответствующую операцию при условии, что вы аутентифицированы на том веб-сайте:

<img src='yoursite/createuser.aspx?id=hacked&pwd=hacked' />

Если вы оказались администратором на «yoursite» и уже аутентифицированы на нем, браузер с радостью отправит ему запрос GET с любыми удостоверениями. Сервер видит, что это корректный запрос от аутентифицированного пользователя, и выполняет этот запрос без вашего ведома, потому что почтовый клиент не получает корректного ответа с изображением, который он мог бы визуализировать.

Как предотвратить CSRF? Чтобы предотвратить CSRF, начните со следующих правил.

  1. Убедитесь, что запрос нельзя воспроизвести простым щелчком ссылки, вызывающей запрос GET. В спецификации HTTP запросы GET предназначены только для получения информации, но не изменения состояния.
  2. Убедитесь, что запрос нельзя воспроизвести, если атакующий использует JavaScript-код для имитации запроса POST формы..
  3. Блокируйте любые операции через GET. Например, не разрешайте создания или удаления записей через URL. В идеале, эти операции должны требовать какого-то взаимодействия с пользователем. Хотя это не предотвратит более изощренные атаки на основе форм, это все же ограничит большое количество атак попроще вроде показанной на примере с тегом image в сообщении электронной почты, а также основанных на использовании базовых ссылок, встроенных на сайты, которые скомпрометированы в результате XSS-атаки.

Предотвращение атак через Web Forms реализуется несколько иначе, чем для ASP.NET MVC. В случае Web Forms можно подписать атрибут MAC для ViewState, что помогает защищаться от подделок, пока вы не устанавливаете EnableViewStateMac в false. Кроме того, стоит подписывать ваш ViewState с использованием текущего сеанса и предотвращать передачу ViewState в строке запроса, чтобы блокировать то, что некоторые называют атакой с одним щелчком (one-click attack) (рис. 7).

Рис. 7. Предотвращение атаки с одним щелчком

void Page_Init(object sender, EventArgs e)
{
  if (Session.IsNewSession)
  {
    // Принудительное создание сеанса; в ином случае
    // идентификатор сеанса меняется при каждом запросе
    Session["ForceSession"] = DateTime.Now;
  }

  // 'Подписываем' ViewState с использованием текущего сеанса
  this.ViewStateUserKey = Session.SessionID;

  if (Page.EnableViewState)
  {
    // Проверяем, чтобы ViewState не передавался в строке
    // запроса. Это помогает избегать атак с одним щелчком.
    if (!string.IsNullOrEmpty(Request.Params["__VIEWSTATE"]) &&
      string.IsNullOrEmpty(Request.Form["__VIEWSTATE"]))
    {
      throw new Exception(
        "Viewstate existed, but not on the form.");
    }
  }
}

Причина, по которой я присваиваю здесь случайное значение сеанса, — убедиться, что сеанс установлен. Вы могли бы использовать любой идентификатор временного сеанса, но в ASP.NET идентификатор сеанса меняется при каждом запросе до тех пор, пока вы сами не создаете сеанс. Здесь нельзя допускать, чтобы идентификатор сеанса менялся при каждом запросе, поэтому вы должны зафиксировать его, создав новый сеанс.

ASP.NET MVC содержит собственный набор встроенных вспомогательных методов, защищающих от CSRF-атак с помощью уникальных маркеров, передаваемых с каждым запросом. Эти вспомогательные методы используют не только обязательное скрытое поле на форме, но и значение cookie, сильно затрудняя поддержку запроса. Эти средства защиты легко реализуются и абсолютно необходимы в ваших приложениях. Чтобы добавить @Html.AntiForgery­Token() в <form> в вашем представлении, сделайте так:

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken();
  @Html.EditorForModel();
  <input type="submit" value="Submit" />
}

Дополняйте все контроллеры, которые принимают переданные данные, атрибутом [ValidateAntiForgeryToken]:

[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult Index(User user)
{
  ...
}

Понимание уязвимости

В статье были рассмотрены кросс-сайтовые скрипты и поддержка кросс-сайтовых запросов — два распространенных способа взлома веб-приложений. Если добавить два типа атак, которые мы обсудили в прошлом номере (встраивание SQL-кода и модификация параметров), то теперь у вас должно сложиться неплохое представление о том, насколько уязвимыми могут быть приложения.

Вы также убедились, насколько легко встраивать средства защиты в ваши приложения, чтобы предотвращаться некоторые наиболее частые атаки. Если вы уже используете эти средства в жизненном цикле разработки своих приложений — замечательно! А если нет, сейчас самое время заняться этим. Вы можете провести аудит существующих приложений индивидуально для каждой его страницы или модуля и в большинстве случаев очень легко переработать их. Кроме того, защищайте свои приложения с помощью SSL, чтобы предотвратить кражу ваших удостоверений. Помните, что о защите нужно думать всегда — до, во время и после разработки.


Эдам Тьюлипер (Adam Tuliper) — архитектор ПО в Cegedim; занимается разработкой ПО более 20 лет. Является официальным спикером в INETA Community и регулярно выступает на конференциях, а также в группах пользователей .NET. Следите за его заметками на twitter.com/AdamTuliper, читайте его блог completedevelopment.blogspot.com или сайт secure-coding.com. For more in-depth information on hack-proofing your ASP.NET applications, see his upcoming Pluralsight video series.

Выражаю благодарность за рецензирование статьи эксперту Барри Доррансу (Barry Dorrans).