Обнаружение и разбор уязвимости CVE-2023-38633 в
librsvg
, заключающейся в ситуации, когда две реализации URL-парсера (Rust и Glib) расходятся в парсинге схемы файла, создавая уязвимость к атаке обхода каталога.
В рамках своей миссии по созданию самой надёжной в мире платформы мы в Canva непрерывно оцениваем веб-безопасность наших программных зависимостей. Определение и устранение уязвимостей в сторонних зависимостях помогает повысить веб-безопасность не только нашей платформы, но и интернета в целом. Вкупе с такими инструментами управления безопасностью, как изолированные среды, мы всё больше усложняем для злоумышленников процесс эксплуатации сторонних зависимостей.
Одной из таких используемых в Canva зависимостей является librsvg (задействуется через libvips). Эта библиотека позволяет скоро отрисовывать пользовательские SVG в пиктограммы, впоследствии отображаемые в виде PNG. Мы показали, что путём эксплуатации различий в отрисовке URL-парсерами SVG-изображений при помощи librsvg можно внедрять в итоговое изображение произвольные файлы с диска. Мейнтейнеры
librsvg скоро исправили эту уязвимость, впоследствии зарегистрированную как CVE-2023-38633.
Мы делимся результатами проведённого исследования в качестве ещё одного примера опасностей, заключающихся в совместном использовании URL-парсеров, особенно с учётом того, что обнаруженный нами случай оказался трудноуловимым.
Отдельная благодарность мейнтейнеру librsvg Федерико, мейнтейнеру libvips Джону и мейнтейнеру Sharp Ловеллу за проделанную ими работу и оперативный отклик.
▍ Предыстория
Статья Виктора Кахана из Elttam на тему
проблем XML-парсинга в Inkscape
показывает, насколько этот инструмент уязвим к атаке обхода каталога при рендеринге SVG. Расширяя исследование Виктора, мы выяснили, что хоть XInclude и не поддерживается в Inkscape 0.9 непосредственно, он демонстрирует интересное поведение, когда одно изображение SVG вложено в другое.
К примеру, взгляните на эту внутреннюю SVG-картинку.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="300" height="300" xmlns:xi="
<rect width="300" height="300" style="fill:rgb(255,204,204);" />
<text x="0" y="100">
<xi:include href=" parse="text" encoding="ASCII">
<xi:fallback>file not found</xi:fallback>
</xi:include>
</text>
</svg>
Мы закодировали её в виде URI и поместили в другое SVG-изображение,
outer.svg
.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="300" height="300" xmlns:xlink="
<image xlink:href="data:image/svg;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIHhtbG5zOnhpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hJbmNsdWRlIj4KICA8cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgc3R5bGU9ImZpbGw6cmdiKDI1NSwyMDQsMjA0KTsiIC8+CiAgPHRleHQgeD0iMCIgeT0iMTAwIj4KICAgIDx4aTppbmNsdWRlIGhyZWY9Ii9ldGMvcGFzc3dkIiBwYXJzZT0idGV4dCIgZW5jb2Rpbmc9IkFTQ0lJIj4KICAgICAgPHhpOmZhbGxiYWNrPmZpbGUgbm90IGZvdW5kPC94aTpmYWxsYmFjaz4KICAgIDwveGk6aW5jbHVkZT4KICA8L3RleHQ+Cjwvc3ZnPg==" />
</svg>
При выполнении с помощью Inkscape 0.92.4 на выходе получилось изображение, в котором активировалась XInclude
fallback
.
$ inkscape -f test.svg -e out.png -w 300

Здесь нас удивил сам факт поддержки XInclude, поскольку это зачастую ведёт к появлению уязвимостей безопасности. И хотя Inkscape в Canva не используется,
анализ его пути выполнения кода
показал, что вложенные изображения загружаются с помощью
GdkPixbuf
, который, в свою очередь, делегирует загрузку SVG библиотеке
librsvg
. Это оказалось очень интересно, потому как
librsvg
в Canva уже используется.
▍ XInclude
XInclude
– это механизм слияния XML-документов, который может создавать уязвимости безопасности, когда пользовательский XML (вроде SVG) формируется или отрисовывается на сервере.

В XInclude выделяется два элемента:
, отвечающий за внедрение содержимого URL, например файла или HTTP-запроса. Включаемое содержимое может быть простым текстом или XML.xi:include
, отвечающий за предоставление содержимого для отрисовки в случае, когдаxi:fallback
не может загрузить то, на которое ведёт ссылка.xi:Include
Если не брать во внимание проверку безопасности, то следующий XML-документ при обработке загружает содержимое
/etc/passwd
.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<example xmlns:xi="
<xi:include href=" parse="text" encoding="ASCII">
<xi:fallback>file not found</xi:fallback>
</xi:include>
</example>
▍ Тут есть правила
librsvg
– это библиотека Rust для отрисовки SVG-изображений на поверхностях
Cairo
. Сейчас основная часть её функциональности реализована на Rust, но при этом она опирается на библиотеки Cairo и GNOME.

Согласно предыстории нам было известно, что
librsvg
поддерживает как минимум некоторые из стандартов XInclude. Чтобы понять, какие именно, мы
изучили её реализацию
. Выяснилось, что каждая внешняя URL-ссылка в SVG для валидации проходит через один метод. В частности, это касается следующих ссылок:
- <image href=«file:///web.archive.org/web/20230911070144/ />
- <rect filter=«url(‘file-with-filters.svg#my_filter’)» />
- <xi:include href=”… />
Метод
librsvg
url_resolver.resolve_href
реализует ряд
строгих проверок безопасности
, определяя, какие ссылки могут быть загружены при обработке SVG-документа:
- все URL
разрешены, поскольку не могут ссылаться на внешние файлы.data:
- схема, на которую ведёт ссылка, должна соответствовать схеме «текущего документа». Например, при обработке
любой встречаемый URL должен соответствовать схемеfile:///web.archive.org/web/20230911070144/
.file:
- встреченные файлы должны находиться в одном каталоге с текущим документом или в его подкаталоге. Это обеспечивается проверкой пути URL.
- все остальные схемы отклоняются, включая
.http:
Эти строгие правила стали причиной провала первых простых тестов XInclude. Но нам захотелось узнать, есть ли вариант их обойти. Это может привести к уязвимости обхода каталога при обработке SVG, например, к возможности включать в содержимое SVG, отрисовываемого в PNG, файлы вроде
/etc/passwd
.
▍ Расхождение парсеров
Разрешение URL-адреса в SVG-документе происходит в два этапа:
- валидация URL согласно описанным выше правилам.
- в случае успеха – загрузка содержимого, которое парсит URL, используя встроенный в Gio парсер URI.
Фрагменты из
mod.rs
и
io.rs
:
// xml/mod.rs
fn acquire(&self, href: Option<&str>, /* ... */) -> Result<(), AcquireError> {
let aurl = self.url_resolver.resolve_href(href) // ...
// ...
self.acquire_text(&aurl, encoding);
}
fn acquire_text(&self, aurl: &AllowedUrl, encoding: Option<&str>) -> Result<(), AcquireError> {
let binary = io::acquire_data(aurl, None);
// ...
return result;
}
// io.rs
pub fn acquire_data(aurl: &AllowedUrl, /* ... */) -> Result<BinaryData, IoError> {
let uri = aurl.as_str();
// ...
let file = GFile::for_uri(uri);
let (contents, _etag) = file.load_contents(cancellable)?;
// ...
return contents;
// ...
}
Зная о том, что здесь задействовано два парсера (один для валидации URL и один для загрузки содержимого), для обхода проверок безопасности нам нужно было найти URL, в котором эти парсеры не согласовывались.
Проведя пару тестов, мы определили, как парсеры обрабатывают разные URL.
Gio
не раскрывает парсинг обобщённого URL-адреса (кроме GUri, который не находится на пути вызова), но в некоторых примерах результат
g_filename_from_uri
возвращается.
▍ Обход валидации
Понимая, где находятся парсеры, мы взяли соответствующие части из
librsvg
и настроили фаззинг-тесты («resolve») для выполнения логики, соответствующей логике обработки URL, при встрече ссылки (href, XInclude и т.п.) из находящегося на диске файла
current.svg
. Это позволило нам скоро протестировать и проанализировать входные данные, чтобы понять, как обрабатывается парсинг и логика валидации. Вот некоторые интересные выводы фаззинга:
: проходит ожидаемым образом.resolve 'current.svg'
: каноникализация проваливается с ошибкойresolve run '../../../../../../../etc/passwd'
‘.'No such file or directory
: проходит.resolve 'current.svg?../../../../../../../etc/passwd'
: проходит ожидаемым образом.resolve 'none/../current.svg'
Последние два результата показали, что
GFile::for_uri
вполне позволяет выполнять обход каталога, в том числе в строке запроса. Тем не менее второй результат,
../../../../../../../etc/passwd
, провалился из-за проверки каноникализации.
▍ Обход каноникализации
Часть валидации URL-адреса в
librsvg
заключается в каноникализации создаваемого URL для замены сегментов
..
и
.
согласно стандартным правилам файловой системы. Библиотека выполняет это, используя
std::fs::canonicalize
(вызывая
realpath
), который выбрасывает ошибку, если:
- путь не существует;
- не последний компонент в пути не является каталогом.
Поскольку мы не всегда знаем имя «current» SVG на диске, для успешного прохождения валидации URL нам нужно было обойти каноникализацию. После небольшого тестирования выяснилось, что это не так трудно.
$ realpath current.svg
/home/zsims/projects/librsvg-poc/current.svg
$ realpath .
/home/zsims/projects/librsvg-poc/
Как оказалось,
realpath(".")
и
std::fs::canonicalize(".")
возвращают «текущий каталог». Мы можем использовать это в нашей проверке концепции в качестве плейсхолдера вместо
current.svg
.
▍ Проверка концепции
Понимая, в чём расходятся URL-парсеры, и как можно обойти каноникализацию, не зная имени текущего файла, мы можем создать полезную нагрузку для внедрения
/etc/passwd
.
.?../../../../../../../etc/passwd
Внутри
poc.svg
это выглядит так:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="300" height="300" xmlns:xi="
<rect width="300" height="300" style="fill:rgb(255,204,204);" />
<text x="0" y="100">
<xi:include href=" parse="text" encoding="ASCII">
<xi:fallback>file not found</xi:fallback>
</xi:include>
</text>
</svg>
И даёт следующий вывод:
$ rsvg-convert poc.svg > poc.png

При выполнении через
vipsthumbnail
мы получаем аналогичный результат.
Чувствительные файлы в /proc, такие как /proc/self/environ, обработать не получилось из-за используемой в них кодировки символов.
$ rsvg-convert proc-poc.svg > proc-poc.png
thread 'main' panicked at 'str::ToGlibPtr<*const c_char>: unexpected '' character: NulError(21...
Заметьте, что эта проверка концепции работает только там, где SVG загружается из
file://
. SVG, загружаемые через схемы
data:
или
resource:
, неуязвимы.
▍ Патч
После получения этого отчёта (
Issue 996
) мейнтейнер
librsvg
Федерико
пропатчил уязвимость
путём улучшения валидации URL и использования в GFile прошедшего эту валидацию URL. Ответ Федерико включал просьбу к мейнтейнерам Sharp и libvips внести исправления до того, как проблема будет публично зарегистрирована под кодом
CVE-2023-38633
.
Данная уязвимость также породила дискуссию на тему парсинга URL-адресов в glib, что привело к реализации в этой библиотеке дополнительной валидации.
В ходе обнаружения и исправления проблемы наиболее яркими оказались следующие моменты:
- опасность совместного использования разных URL-парсеров в той же степени касается URL
и внутрипроцессного использования, в какой сетевых сервисов и URLfile://
.http://
- URL
являются особенными. Например, в спецификации URL подчёркивается поддержка строк запроса в адресахfile://
, но в изученных нами реализациях поддержка мощно отличалась.file://
- усилия по преобразованию существующего кода Си в код Rust сопряжены с риском. И хотя веб-безопасность памяти значительно повысилась, различия в контрактах (вроде парсинга URL) могут сказаться на ней негативным образом.
- необходимо прослеживать, чтобы URL парсился только один раз, и далее использовалось именно полученное значение.
- по возможности нужно реализовывать собственную валидацию файлов, таких как SVG, до их последующей обработки. По этой причине MediaWiki не подвержена данной проблеме, так как элементы
отклоняются до того, как SVG достигает librsvg.xi:include
▍ Хронология
- 11 июля 2023: обнаружена проблема;
- 12 июля 2023: информация передана мейнтейнерам librsvg.
- 19 июля 2023: мейнтейнеры librsvg сообщают о проблеме мейтейнерам зависимых библиотек, включая libvips и Sharp.
- 21 июля 2023: librsvg пропатчена.
- 22 июля 2023 – уязвимость зарегистрирована под кодом CVE-2023-38633.
Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх 🕹️
2023-09-15 17:00:01