|
Professor Seleznov
|
Рендерер Scratch имеет долгую историю связанных с SVG уязвимостей. Их источником становится то, что Scratch парсит сгенерированный пользователем (то есть контролируемый нападающими) контент в элемент и добавляет его в основной документ для выполнения различных операций (например, для измерения ограничивающего прямоугольника SVG более надёжным образом, чем viewbox или width/height). Даже если SVG остаётся в основном документе очень недолго, это небезопасная по своей природе операция. Для обеспечения защиты Scratch реализовывал всё более сложную инфраструктуру парсинга SVG и находящейся внутри разметки, чтобы устранить опасные части. Я считаю, что подход Scratch к санации SVG обречён на провал. Чтобы объяснить это, нам нужно совершить путешествие по истории санации SVG в Scratch и посмотреть, насколько хорошо он с этим справлялся. 2019 год: XSS при помощи тэга В 2019 году, спустя несколько месяцев после выпуска Scratch 3, разработчики Scratch обнаружили, что SVG могут содержать тэги , исполнение которых при загрузке SVG обеспечивает Scratch. Такая атака называется XSS. В Scratch атака XSS позволяет нападающему выполнять действия от лица того, кто загрузит его проект. Например, нападающий может публиковать комментарии, удалять проекты или пытаться захватить аккаунт жертвы иными способами. В Scratch Desktop XSS переходит в исполнение произвольного кода, потому что Scratch Desktop включает опасную фичу интеграции Node.js Electron. (В TurboWarp Desktop эта фича не включена с v0.2.0 от марта 2021 года). Пример из набора тестов Scratch:
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
Проблема была устранена при помощи регулярного выражения, удаляющего тэги script. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2020 год: XSS из-за ошибок в предыдущем исправлении (CVE-2020-7750) В 2020 году apple502j обнаружил, что XSS всё ещё возможен. Оказалось, что предыдущее исправление абсолютно поломанное и его можно обойти, написав заглавными буквами, потому что регулярное выражение учитывало регистр; было и множество других способов обхода. Даже если бы регулярное выражение реализовали корректно, это всё равно бы не сработало, потому что существуют и другие способы встраивания JavaScript в SVG. Например, можно использовать встроенный обработчик событий:
xmlns="http://www.w3.org/1999/xhtml" src="data:any invalid URL" onerror="alert(1)" />
Проблема была устранена при помощи DOMPurify, удаляющего скрипты из SVG перед тем, как scratch-svg-renderer добавляет их в документ. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2022 год: HTTP-утечка через href В 2022 году обнаружилось, что при помощи свойства href элемента нападающий может создать SVG, который при загрузке вызывает внешний запрос. Оказалось, что хоть DOMPurify и удаляет исполняемый код, он не защищает от HTTP-утечек, потому что «существует слишком много способов её реализации и наши тесты показали, что неё нельзя защититься надёжным образом». Для Scratch HTTP-утечка означает, что пользователь Scratch может записывать IP-адрес любого, кто загружает его проект, потенциально раскрывая такую информацию, как местоположение или школьный округ. Жертве не нужно нажимать ни на какие ссылки; логгинг IP-адреса происходит просто при загрузке проекта. Похоже, разработчики Scratch посчитали это багом безопасности, и я согласен с ними. Пример:
Проблема была решена добавлением хуков DOMPurify для удаления свойств href из всех элементов, если URL ссылается на удалённый сайт. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2023 год: HTTP-утечка через @import CSS В 2023 году обнаружилось, что при помощи правила @import CSS внутри элемента нападающий может создать проект, создающий внешние запросы при загрузке проекта. Пример:
Проблема была решена интеграцией написанного на JavaScript парсера CSS, который удаляет опасные части CSS. Он парсит все содержащиеся в SVG таблицы стилей, удаляет все правила @import, и в случае внесения изменений преобразует CSS обратно в строку. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2024 год: XSS через Paper.js В 2024 году я обнаружил XSS в Paper.js — библиотеке, которую Scratch использует в редакторе костюмов. Оказалось, что хотя Scratch санировал SVG перед работой с ними в scratch-svg-renderer, Paper.js передавались несанированные SVG. В основном эта уязвимость представляла такую же угрозу, как XSS scratch-svg-renderer, обнаруженное в 2020 году, но возникала при использовании редактора костюмов, а не при открытии проекта. Пример:
xmlns="http://www.w3.org/1999/xhtml" src="data:any invalid URL" onerror="alert(1)" />
Проблема была частично решена за очень долгий период времени благодаря расширению кода санации SVG: теперь он запускался при загрузке SVG, а не только при его обработке в scratch-svg-renderer. С этого момента Paper.js получает только уже санированные SVG. Я написал «частично решена», потому что не знаю, выполняется ли вообще санация для скачиваемых сервером SVG. В поддержке Scratch мне сказали, что у них «есть меры защиты против того, что обрабатывается на стороне сервера», из-за чего такая санация была бы избыточной. При разработке proof-of-concept я ни разу не видел признаков такой защиты, но, возможно, она реальна. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2025 год: HTTP-утечка через url() CSS В 2025 году выяснилось, что при использовании url() внутри некоторых правил CSS нападающий может создать SVG, при загрузке создающий внешний запрос. Примеры:
https://example.com/ping)" />
Проблема была решена существенным расширением кода санации SVG: теперь он искал любые вхождения url() и удалял все стили или атрибуты, ссылающиеся на внешние URL. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2026 год: HTTP-утечка через множество багов в старом коде В 2026 году обнаружилось, что при использовании url() внутри некоторых правил CSS нападающий по-прежнему может создать SVG, при загрузке совершающий внешний запрос. Оказалось, что эта HTTP-утечка стала возможной благодаря как минимум трём уникальным багам:
- Не учтено то, что CSS позволяет записывать url(...) при помощи управляющих последовательностей.
- Не обрабатывалась ситуация, при которой атрибут style содержал несколько url(...), где первый безопасен, а второй нет.
- Не обрабатывался url(), определённый в переменной CSS, на который ссылаются через var(—name).
Пример:
https://example.com/ping)" /> https://example.com/ping)" />
Проблема была решена добавлением большого объёма дополнительной сложности вокруг кода, который и так уже был слишком сложным. Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений. 2026 год: полная смена стилей страницы при помощи долгих переходов В 2026 году выяснилось, что хитро использовав очень долгие переходы и заставив браузер изменить стили всех элементов, нападающий может применять ко всей странице Scratch произвольные стили, сохраняющиеся до обновления страницы. Чаще всего эта уязвимость использовалась для развлечений, но её можно применять и для более зловещих действий:
- Прятать кнопку «Пожаловаться».
- Сделать кнопки лайков/добавления в избранное размером со всю страницу, чтобы пользователи вынуждены были их нажимать.
- Отображать текст, сообщающий пользователю, что ему нужно открыть веб-сайт в новой вкладке, чтобы «верифицировать» свой аккаунт (на какой-нибудь фишинговой странице). Пользователи, скорее всего, поверят инструкциям, потому что сообщение поступает от реального scratch.mit.edu.
Пример проекта (не мой): https://scratch.mit.edu/projects/1299571218/ Рано или поздно это, наверно, исправят, но пока пользователь будет видеть такое:
 В этом проекте используются два SVG. Первый из них — это «триггер»:
Trigger
Второй содержит стили для отображения:
Styles
Не буду делать вид, что понимаю происходящее здесь, и почему это работает недетерминированно, но в целом представляю это так:
- Триггерный SVG применяет transform и filter к каждому элементу документа, чтобы вынудить браузер сразу же заново вычислить все стили, применив стили из другого SVG.
- Триггерный SVG применяет очень долгий transition, чтобы после удаления другого SVG стили сохранялись в течение всего «перехода».
Эта проблема не решена. Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений. 2026 год: HTTP-утечка через image-set() Я сообщал о ней разработчикам Scratch в 2025 году. Они её не устранили, поэтому я раскрываю её в этой статье. Все разумные сроки раскрытия прошли уже полгода назад. Вместо url() нападающий может использовать image-set(), чтобы создать SVG, при загрузке выполняющий внешний запрос. Примеры:
Эта проблема не решена. Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений. 20XX год: HTTP-утечка через новые фичи CSS Об этом я тоже сообщал разработчикам Scratch в 2025 году. На самом деле, этот баг пока не работает, но начнёт работать в будущем, если браузеры реализуют все CSS Units Level 4 или CSS Images Level 4. Сегодня Ladybird — единственный реализующий их браузер, но рано или поздно их могут реализовать и самые популярные браузеры. Вместо url() нападающий может использовать src() или image(), чтобы создать SVG, при загрузке совершающий внешний запрос. Примеры:
Эта проблема не решена. Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений. Такая система неустойчива Засовывание в процесс санации всё больше сложности — это обречённое на провал решение. Мы уже углубились на пять крупных доработок, но до сих пор существуют известные дыры. Люди активно делятся проектами на веб-сайте Scratch, обходя санацию SVG. А в момент, когда в браузерах решат реализовать последние спецификации CSS, откроется ещё больше дыр. Кроме того, не у всех этих проблем есть чёткие решения. В случае уязвимости с полной стилизацией страницы оба SVG выглядят совершенно невинно: в них нет JavaScript и ссылок на внешние ресурсы. Вероятно, устранить проблему можно было бы, удалив стили transition, потому что в Scratch переходы всё равно никогда не выполняются, но уверены ли мы, что этого достаточно? Вспомним ли мы, что нужно удалить все версии transition с префиксами поставщика? А что насчёт стилей animation? Вот некоторые другие примеры, которые могут обеспечить возможность обхода защиты в будущем:
- css-tree (библиотека, используемая Scratch для парсинга CSS) и реальные парсеры CSS браузеров могут совпадать не полностью. В этом случае css-tree может спарсить CSS так, что всё выглядит правильно, а значит, ничего не удалится, но реальный парсер браузера потом распознает внешний контент.
- Продвинутые новые фичи CSS наподобие @property или native nesting, которые версии css-tree, возможно, не смогут осмысленно парсить без постоянных обновлений.
- Браузеры всегда могут добавить новые функции, способные ссылаться на внешний контент, как это произошло с image-set() и с тем, что подразумевает спецификация в src() и image(). Как не отставать от постоянных изменений в этих спецификациях и проверять, не ссылается ли каждая новая функция на внешний контент?
Альтернатива TurboWarp (форк Scratch, над которым работаю я) не затронули HTTP-утечки 2026 года и проблема полной смены стилей страницы. И не потому, что я нашёл все хитрые способы, которыми SVG могут наносить вред: на самом деле, я полностью удалил код санации CSS, чтобы упакованные проекты стали на 400 КБ меньше. Я реализовал альтернативное решение для сэндбоксинга SVG внутри iframe. Сначала мы создаём iframe со свойством sandbox, равным allow-same-origin. Это не позволяет исполнять скрипты снаружи iframe, но позволяет при этом взаимодействовать с контентом внутри. Во-вторых, мы создаём iframe со следующим HTML:
Встроенная Content-Security-Policy настроена так, чтобы блокировать все скрипты и позволять загружать только безопасные ресурсы из URL безопасных данных. Также мы по-прежнему используем DOMPurify для устранения из SVG очевидно зловредных вещей. Затем мы помещаем iframe в какую-нибудь часть документа за пределами экрана, чтобы необходимый Scratch API измерений продолжал работать. Такое решение обеспечивает нам очень удобные свойства:
- Браузер использует готовый код, чтобы выполнять за нас самую сложную работу.
TurboWarp не обязан знать о всех способах, которыми SVG может выполнять запрос. Их уже знает браузер, и он будет проверять их для всех новых добавляемых API. Реальные реализации CSP неидеальны и содержат дыры. Однако эти дыры обычно оказываются странными пограничными случаями, требующими от нападающего обеспечить исполнение JavaScript. Такие уязвимости считаются проблемами безопасности браузеров, поэтому за них платят баг-баунти.
- SVG не может влиять на основной документ.
Возьмём для примера смену стилей всей страницы. Так как SVG заключён в iframe, он может изменить стили только этого iframe. Стили iframe ни на что не влияют, так что нас это устраивает.
Наш код можно найти здесь:
Вероятно, можно делать что-то интересное с shadow DOM или другими веб-API, но нас вполне устраивает решение с iframe. Ниже я расскажу о проблемах, о которых узнал после публикации статьи. 12 апреля 2026 года: Claude нашёл HTTP-утечку через расслабленный синтаксис вложенности CSS После публикации статьи мне стало интересно, насколько хорошо современные языковые модели умеют находить подобные баги. Я попросил Claude Opus 4.6 клонировать репозиторий scratch-editor, изучить последние изменения в рендерере SVG и поискать в них дыры. Результаты оказались интересными:
- Claude самостоятельно обнаружил, что image-set(...) не санируется и может вызывать HTTP-утечки.
- Claude обнаружил новую проблему, не описанную в этом посте.
Баг связан с вложенностью CSS, которая может проявляться в двух формах. Вложенный стиль может добавлять к селектору префикс & или не добавлять префикс (последнее известно, как «расслабленный» синтаксис). Современные браузеры интерпретируют оба показанных ниже примера одинаково.
g { & rect { background-image: url(https://example.com/ping); } } g { rect { background-image: url(https://example.com/ping); } }
css-tree способен парсить версию с префиксом & в осмысленное дерево синтаксиса, которое способен санировать Scratch. Однако оказалось, что css-tree не знает, как парсить расслабленную версию. Весь блок div { ... } парсится, как узел «сырого текста», который код Scratch не санирует. Вот полный пример SVG:
Ранее в этом посте я говорил, что css-tree и реальные парсеры CSS браузеров могут совпадать не полностью. Вот реальный пример бага, позволяющего обойти санацию CSS. Стоит отметить, что сейчас у css-tree есть 48 открытых issue и множество других неизвестных проблем. Я считаю, что надежда на то, что css-tree будет идеальным парсером — тупиковый путь, который приведёт к ещё большему количеству уязвимостей. Песочница SVG в TurboWarp полностью устранила этот баг, хотя я о нём даже не знал. Эта проблема не устранена. Issue css-tree по этому багу открыта с декабря 2023 года. Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений.-Источник
|