Почему моё приложение при открытии SVG-файла отправляет сетевые запросы?
Вы решили сделать приложение, работающее с SVG. Набрали библиотек, запаслись энтузиазмом, и в итоге всё удалось. Но вот незадача! Внезапно вы обнаруживаете, что приложение отправляет странные сетевые запросы. Кроме того, с хост-машины утекают данные. Как же так?
В современном мире на каждый случай жизни есть библиотека. Поэтому для своего приложения мы также не будем изобретать велосипед, а возьмём готовое решение. Например, SVG.NET. Исходный код проекта доступен на GitHub. Сама библиотека дистрибьютится как NuGet-пакет, что очень удобно в плане подключения к проекту. Кстати, на странице проекта в NuGet Gallery можно увидеть, что библиотеку загрузили 2.5 миллиона раз — впечатляет!
Рассмотрим синтетический пример описанного ранее приложения:
void ProcessSvg()
{
using var svgStream = GetSvgFromUser();
var svgDoc = SvgDocument.Open(svgStream);
// SVG document processing...
SendSvgToUser(svgDoc);
}
Суть проста:
- Получаем от пользователя картинку. Как именно — не принципиально.
- Создаётся экземпляр SvgDocument, с которым дальше осуществляются какие-то действия. Например, некоторые преобразования.
- Изменённый объект отправляется обратно пользователю.
Реализация методов GetSvgFromUser и SendSvgToUser в данном случае не столь важна. Будем считать, что первый принимает картинку по сети, а второй отправляет её обратно.
Что скрывается за «SVG document processing…»? И вновь здесь нам это не важно, так что у нас… ничего не будет.
По факту мы просто загружаем картинку и сохраняем её обратно. Просто? Достаточно, чтобы начали происходить странные вещи. :)
Для экспериментов возьмём специально заготовленный SVG-файл. Внешне он выглядит как логотип анализатора PVS-Studio. Посмотрим на его отрисовку в браузере, чтобы убедиться, что всё с ним в порядке.
Никаких проблем нет. Отправляем в наше приложение. Оно никаких операций над изображением не проводит (напоминаю, что за комментарием в коде ничего не скрывается) и просто отправляет SVG нам обратно.
Открываем полученный файл и ожидаемо видим ту же картину.
Самое интересное произошло за кулисами (во время вызова метода SvgDocument.Open
Первое — приложение отправило незапланированный запрос к pvs-studio.com. Это можно было увидеть, например, отмониторив сетевую активность приложения.
Второе — пользователь приложения получил файл hosts с машины, на которой открывался SVG.
Как? Где этот файл? Давайте посмотрим на текстовое представление SVG-файла, полученного от приложения. Ненужные части сократим, чтобы не мешались.
Вот и hosts файл с целевой машины — аккуратно спрятан в SVG-файле без каких-либо внешних проявлений.
Откуда там взялось содержимое hosts? Откуда дополнительный сетевой запрос? Что ж, давайте разбираться.
Разбираем атаку
Те, кто знаком с XXE-атакой, возможно, уже поняли, в чём дело. Если про XXE вы не слышали или подзабыли, что это такое — настоятельно рекомендую ознакомиться со статьёй «Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике». В ней я рассказываю о сути XXE, причинах и последствиях. Эта информация потребуется для понимания дальнейшего изложения.
Напомню, что для проведения XXE-атаки необходимы:
- данные от пользователя, которые могут быть скомпрометированы;
- небезопасно сконфигурированный XML-парсер.
Злоумышленнику также на руку будет, если ему в каком-то виде вернётся результат обработки скомпрометированных данных XML-парсером.
В данном случае «все звёзды совпали»:
- скомпрометированные данные — SVG файл, который пользователь отправляет в приложение;
- небезопасно сконфигурированный XML-парсер — есть, находится внутри библиотеки открытия SVG-файла;
- результат работы парсера возвращается обратно пользователю в виде «обработанного» SVG-файла.
Скомпрометированные данные
Первое, что нужно вспомнить — формат SVG основан на XML. Это даёт возможность определять в SVG-файлах XML-сущности, которые и нужны для проведения XXE.
Несмотря на то, что в браузере SVG-файл «подставной» выглядит обычным образом, внутри он содержит объявление двух сущностей:
]>
Если XML-парсер работает с внешними сущностями, то:
- при обработке queryEntity он выполнит сетевой запрос к files.pvs-studio.com;
- при обработке hostsEntity вместо сущности он подставит содержимое файла hosts.
Получается своего рода SVG-ловушка: при отрисовке файл выглядит обычным, но внутри оказывается с подвохом.
Небезопасно сконфигурированный XML-парсер
Стоит помнить, что за использование внешних библиотек приходится платить свою цену. Если у вас уже был лист возможных негативных последствий, добавляйте к нему ещё одно — потенциальные дефекты безопасности.
Для создания экземпляра SvgDocument мы использовали метод Open
public static T Open(Stream stream) where T : SvgDocument, new()
{
return Open(stream, null);
}
Этот метод, в свою очередь, вызывает другую перегрузку:
public static T Open(Stream stream, Dictionary 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(reader);
}
Забегая вперёд, хочется сказать, что в Open
private static T Open(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:
///
/// Skip the Dtd Processing for faster loading of
/// svgs that have a DTD specified.
/// For Example Adobe Illustrator svgs.
///
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 обрабатывать внешние сущности. По умолчанию обработка выключена.
Во втором PR кода докинули побольше, а булев флаг заменили на перечисление. По умолчанию резолвинг внешних сущностей всё так же запрещён. Изменений в коде побольше, если интересно — посмотреть их можно здесь.
Если обновить пакет 'Svg' до безопасной версии, запустить в том же приложении и с теми же входными данными (то есть с подставным SVG-файлом), получим другие результаты.
Приложение больше не выполняет сетевых запросов, равно как и не «крадёт» файлы. Если посмотреть результирующий 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?.