Обнаружение и разбор уязвимости 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="http://www.w3.org/2001/XInclude">
<rect width="300" height="300" style="fill:rgb(255,204,204);" />
<text x="0" y="100">
<xi:include href="/etc/passwd" 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="http://www.w3.org/1999/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 выделяется два элемента:
xi:include
, отвечающий за внедрение содержимого URL, например файла или HTTP-запроса. Включаемое содержимое может быть простым текстом или XML.xi:fallback
, отвечающий за предоставление содержимого для отрисовки в случае, когдаxi:Include
не может загрузить то, на которое ведёт ссылка.
Если не брать во внимание проверку безопасности, то следующий XML-документ при обработке загружает содержимое /etc/passwd.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<example xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="/etc/passwd" 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/https://something.png» />
- <rect filter=«url('file-with-filters.svg#my_filter')» />
- <xi:include href="/etc/passwd"… />
Метод librsvg url_resolver.resolve_href
реализует ряд строгих проверок безопасности, определяя, какие ссылки могут быть загружены при обработке SVG-документа:
- все URL
data:
разрешены, поскольку не могут ссылаться на внешние файлы. - схема, на которую ведёт ссылка, должна соответствовать схеме «текущего документа». Например, при обработке
file:///web.archive.org/web/20230911070144/https://foo/bar/example.svg
любой встречаемый URL должен соответствовать схемеfile:
. - встреченные файлы должны находиться в одном каталоге с текущим документом или в его подкаталоге. Это обеспечивается проверкой пути URL.
- все остальные схемы отклоняются, включая
http:
.
Эти строгие правила стали причиной провала первых простых тестов XInclude. Но нам захотелось узнать, есть ли вариант их обойти. Это может привести к уязвимости обхода каталога при обработке SVG, например, к возможности включать в содержимое SVG, отрисовываемого в PNG, файлы вроде /etc/passwd.
▍ Расхождение парсеров
Разрешение URL-адреса в SVG-документе происходит в два этапа:
- валидация URL согласно описанным выше правилам.
- в случае успеха – загрузка содержимого, которое парсит URL, используя встроенный в Gio парсер URI.
// 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.
URL file:///web.archive.org/web/20230911070144/https://etc/passwd?foo=bar#baz |
Спарсенный результат Rust url |
Схема | file |
Хост | нет |
Путь | /etc/passwd |
Запрос | foo=bar |
Фрагмент | baz |
Gio не раскрывает парсинг обобщённого URL-адреса (кроме GUri, который не находится на пути вызова), но в некоторых примерах результат g_filename_from_uri
возвращается.
URL | результат g_filename_from_uri |
file:///web.archive.org/web/20230911070144/https://etc/passwd?foo=bar#baz | /etc/passwd?foo=bar#baz Примечание: в коммите Glib 3986471 даёт сбой из-за символов? и #. |
file:///web.archive.org/web/20230911070144/https://etc/passwd | /etc/passwd |
file:///web.archive.org/web/20230911070144/https://etc/passwd&hello | /etc/passwd&hello |
file:///web.archive.org/web/20230911070144/https://etc/passwd@host | /etc/passwd@host |
file://host/etc/passwd | /etc/passwd |
▍ Обход валидации
Понимая, где находятся парсеры, мы взяли соответствующие части из 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="http://www.w3.org/2001/XInclude">
<rect width="300" height="300" style="fill:rgb(255,204,204);" />
<text x="0" y="100">
<xi:include href=".?../../../../../../../etc/passwd" 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
file://
и внутрипроцессного использования, в какой сетевых сервисов и URLhttp://
. - URL
file://
являются особенными. Например, в спецификации URL подчёркивается поддержка строк запроса в адресахfile://
, но в изученных нами реализациях поддержка сильно отличалась. - усилия по преобразованию существующего кода Си в код Rust сопряжены с риском. И хотя безопасность памяти значительно повысилась, различия в контрактах (вроде парсинга URL) могут сказаться на ней негативным образом.
- необходимо прослеживать, чтобы URL парсился только один раз, и далее использовалось именно полученное значение.
- по возможности нужно реализовывать собственную валидацию файлов, таких как SVG, до их последующей обработки. По этой причине MediaWiki не подвержена данной проблеме, так как элементы
xi:include
отклоняются до того, как SVG достигает librsvg.
▍ Хронология
- 11 июля 2023: обнаружена проблема;
- 12 июля 2023: информация передана мейнтейнерам librsvg.
- 19 июля 2023: мейнтейнеры librsvg сообщают о проблеме мейтейнерам зависимых библиотек, включая libvips и Sharp.
- 21 июля 2023: librsvg пропатчена.
- 22 июля 2023 – уязвимость зарегистрирована под кодом CVE-2023-38633.
Автор: Дмитрий Брайт