14 de set. de 2025·8 min de leitura

Vulnerabilidades Markdown XSS: passos para sanitização segura de rich-text

Vulnerabilidades Markdown XSS podem se esconder em comentários e notas. Aprenda sanitização segura de HTML, restrições de embeds e como testar payloads reais antes do lançamento.

Vulnerabilidades Markdown XSS: passos para sanitização segura de rich-text

Por que Markdown e rich-text podem virar um bug de segurança

Markdown e editores rich-text parecem seguros porque parecem apenas escrita. Mas muitas aplicações fazem a mesma coisa por baixo: pegam o que o usuário digitou, convertem para HTML e rendem esse HTML no navegador de outra pessoa.

É nessa última etapa que o problema começa. Se um atacante conseguir infiltrar HTML que o navegador trate como código, ele pode executar JavaScript na sessão de outro usuário. Isso é XSS (cross-site scripting) em termos simples: o script do atacante roda como se tivesse vindo do seu site, com o acesso da vítima.

Comentários, notas e respostas de suporte são caminhos comuns de ataque porque são fáceis de postar (muitas vezes sem revisão), são exibidos para muitas pessoas (admins, colegas, clientes) e são armazenados e re-renderizados depois. Uma postagem maligna pode continuar a prejudicar por meses.

A parte complicada é que editores Markdown e rich-text frequentemente geram HTML que você não esperava. Um “simples” colar do Google Docs pode trazer tags e atributos estranhos. Alguns setups de Markdown permitem HTML cru de propósito. Alguns editores geram atributos que você nunca planejou suportar. Se sua app renderiza esse output diretamente, você pode acabar com vulnerabilidades Markdown XSS mesmo quando a interface parece inofensiva.

Um exemplo realista: um fundador adiciona um recurso de notas onde colegas colam trechos e formatam texto. Alguém cola conteúdo que inclui um handler de evento como onerror em uma imagem. Se seu renderizador mantém isso, toda vez que um admin abrir a nota, o navegador executa o payload.

A renderização de conteúdo é uma feature de segurança, não apenas uma escolha de formatação.

Os riscos básicos de XSS em conteúdo gerado pelo usuário

XSS acontece quando sua app mostra input do usuário como se fosse conteúdo confiável da página. Com Markdown e rich-text, o risco aumenta porque você frequentemente converte o input para HTML e o renderiza no navegador de outras pessoas.

Para comentários, notas e perfis, o maior problema costuma ser XSS armazenado. Alguém posta um “comentário” que contém HTML ou JavaScript sorrateiro. Seu servidor salva, e toda pessoa que vê esse tópico executa o código do atacante.

XSS refletido é o outro tipo comum: o payload é devolvido imediatamente (frequentemente via URL ou campo de busca). Também importa, mas XSS armazenado é o que continua a te atingir ao longo do tempo.

As vítimas não são apenas usuários aleatórios. Funcionários muitas vezes correm maior risco porque veem mais conteúdo: moderadores revisando relatórios, agentes de suporte checando tickets e admins abrindo dashboards que mostram posts de usuários.

Se um XSS armazenado acertar, um atacante pode roubar cookies de sessão ou tokens de acesso, ler dados privados mostrados na UI (mensagens, emails, detalhes de cobrança), executar ações como a vítima (postar, apagar, mudar configurações) ou enganar usuários com uma UI falsa (phishing dentro do seu próprio site).

“Só usuários internos podem acessar isto” ainda é arriscado. Contas internas costumam ter mais poder, reutilizam senhas e são confiadas por outros sistemas. Um comentário malicioso pode pular de um usuário com pouca permissão para uma conta de staff rapidamente.

Um exemplo simples: um usuário posta uma nota que parece normal, mas inclui um handler de evento escondido em um HTML permitido. Quando um agente de suporte clica para expandir a nota, o payload é executado e envia silenciosamente a sessão dele ao atacante. É assim que vulnerabilidades Markdown XSS se tornam takeover de contas.

Onde o HTML inseguro se infiltra

A maioria das equipes espera problemas apenas quando permitem HTML cru. A surpresa é que você pode ter vulnerabilidades Markdown XSS mesmo quando usuários só digitam Markdown “normal”.

Recursos do Markdown compilam para HTML que o navegador vai interpretar felizmente se você não limpar. Links e imagens são os grandes vilões: um [texto](...) ou ![](...) que parece inofensivo vira uma tag \u003ca\u003e ou \u003cimg\u003e. O perigoso nem sempre é a tag em si, mas o esquema de URL e os atributos que acabam dentro dela.

Alguns parsers de Markdown também permitem blocos de HTML cru por padrão. Isso significa que um usuário pode colar \u003cimg onerror=...\u003e ou \u003csvg\u003e direto em um comentário e isso passar sem mudança, depois rodar quando renderizado. Mesmo que você ache “nós escapamos HTML”, verifique as configurações do parser e plugins que reabilitam isso.

Editores rich-text podem ser piores porque geram HTML que parece inofensivo à primeira vista. Uma frase em negrito curta pode incluir atributos extras, estilos inline e tags estranhas que seu sanitizador tem que entender corretamente. Uma falha comum é permitir tags “seguras” mas esquecer atributos perigosos, como handlers de evento (onload, onclick) ou atributos que carregam URLs que podem esconder payloads javascript:.

Copiar e colar do Google Docs ou Notion é uma fonte frequente de marcação confusa. Usuários colam texto formatado e, de repente, você tem spans aninhados, CSS inline e atributos de metadados que nunca fizeram parte do seu plano. Esse HTML extra aumenta a chance de bypass ou de seu sanitizador quebrar a formatação de maneiras imprevisíveis.

Durante a revisão, foque nos pontos de entrada que repetidamente causam problemas: HTML cru habilitado no parser Markdown; links ou imagens onde a URL não tem esquemas restritos; listas de “atributos permitidos” muito amplas; plugins que adicionam recursos HTML (tabelas, menções, embeds); e caminhos de colagem que aceitam HTML completo em vez de texto simples.

Escolha um modelo de conteúdo seguro antes de escolher um sanitizador

A maioria dos bugs de XSS armazenado começa com um desalinhamento: você achou que estava armazenando “comentários”, mas seu sistema está, na prática, armazenando mini páginas web. Antes de escolher qualquer biblioteca, decida o que os usuários podem expressar.

Uma maneira prática de pensar sobre vulnerabilidades Markdown XSS é escolher um modelo de conteúdo e segui‑lo:

  • Texto simples: o mais seguro e fácil. Ainda dá para suportar quebras de linha e auto-linking simples.
  • Markdown limitado: bom para a maioria dos produtos. Permite formatação (negrito, itálico, listas, código) mas mantém tudo previsível.
  • Rich-text completo (similar a HTML): risco mais alto. Só escolha se realmente precisar de layouts complexos.

Depois de escolher, escreva suas regras numa allowlist. Para Markdown limitado, um conjunto típico e seguro de elementos é: p, strong, em, ul, ol, li, code, pre e a. Mantenha a lista curta de propósito.

Também seja explícito sobre o que nunca é permitido. Os óbvios são script, iframe, object, embed e style. Mas muitas tags “normais” podem ser perigosas dependendo da sua configuração, especialmente qualquer coisa que possa carregar conteúdo remoto ou afetar a página.

Atributos precisam do mesmo tratamento. Por exemplo, links podem permitir apenas href, e você rejeita qualquer coisa que pareça um handler de evento (onclick, onerror e semelhantes).

Como sanitizar HTML com segurança (sem quebrar tudo)

A maneira mais segura de lidar com vulnerabilidades Markdown XSS é supor que o conteúdo do usuário e suas dependências vão mudar ao longo do tempo. Seu parser Markdown atualiza, navegadores mudam, e tags “inofensivas” podem ganhar novos comportamentos.

Por isso sanitizar apenas quando alguém salva um post é arriscado. Sanitize novamente quando você renderiza, assim conteúdo antigo fica seguro depois de uma atualização de dependência ou uma mudança de configuração.

Uma regra prática: prefira uma allowlist. Blocklists tendem a perder casos de borda (novas tags, atributos estranhos, peculiaridades do navegador). Uma allowlist responde a uma pergunta clara: “O que permitimos nos comentários?” Normalmente é formatação básica, links simples e nada que execute código.

Antes de sanitizar, normalize o que você vai sanitizar. Atacantes usam truques como caracteres codificados e espaços incomuns para burlar filtros. Decodifique entidades, normalize Unicode e parseie numa árvore HTML real (não regex). Então execute o sanitizador nessa representação normalizada para que variações de escrita não enganem o filtro.

Um fluxo prático que evita a maior parte dos problemas:

  • Parseie Markdown para HTML usando uma biblioteca confiável.
  • Normalize e decodifique o HTML (entidades, Unicode, espaçamento de atributos).
  • Sanitize com uma allowlist (tags + atributos + esquemas de URL).
  • Renderize o output sanitizado e defina uma Política de Segurança de Conteúdo (Content Security Policy) separadamente.
  • Logue ou marque conteúdos que foram muito removidos (frequentemente sinal de sondagem).

Mantenha as regras do sanitizador em um só lugar e trate‑as como código. Versione a configuração, adicione um curto changelog e escreva alguns testes que afirmem o que deve ser mantido vs removido. Exemplo: se depois decidir permitir \u003cimg\u003e, mude a allowlist e atualize os testes, então re‑sanitize em tempo de render para que comentários antigos não se tornem perigosos.

Second Pair of Eyes
Get expert verification on top of AI tooling to catch real XSS payload paths.

Links, imagens e estilos são onde o Markdown “seguro” frequentemente vira XSS armazenado. Mesmo se você sanitizar tags HTML, é preciso tratar toda URL e todo valor de estilo como input não confiável.

Comece pelos links. Um anchor que parece normal pode virar ataque se você permitir esquemas de URL arriscados como javascript: ou data:. A regra mais segura é simples: permita apenas https: (e talvez http: para ferramentas internas ou dev), e rejeite o resto. Normalize e decodifique antes de checar, porque atacantes usam casos mistos e caracteres codificados.

Se você abrir links do usuário em uma nova aba com target="_blank", proteja‑os. Sem os valores rel corretos, a nova página pode controlar a aba original (tabnabbing). Faça disso um comportamento padrão do seu renderizador em vez de depender dos autores.

Imagens não são “apenas imagens”. Um src pode apontar para pixels de rastreamento, recursos de rede interna ou esquemas esquisitos. Se permitir imagens, restrinja os esquemas de src, e considere fazer proxy das imagens para que o navegador não as busque diretamente de servidores controlados por atacantes.

Estilos são o perigo silencioso. Mesmo quando CSS não executa script, ele pode esconder avisos, mover botões ou fazer uma caixa de login falsa parecer real. Para segurança em rich-text, prefira uma allowlist mínima de formatação simples (negrito, itálico, listas) e evite permitir CSS arbitrário.

Conjunto prático de regras:

  • Permita apenas https: (opcionalmente http:) em href e src; bloqueie javascript:, data:, file: e blob:.
  • Se target="_blank" for usado, force rel="noopener noreferrer".
  • Remova style inline e bloqueie \u003cstyle\u003e inteiramente.
  • Mantenha o suporte a imagens minimal, ou faça proxy e cache no servidor.
  • Defina limites claros: comprimento máximo de URL, máximo de atributos, máximo de elementos.

Exemplo: um app de notas renderiza Markdown para HTML e permite imagens. Um atacante posta um “diagrama útil” que é carregado do servidor dele, loga cada visualização e usa CSS para esconder o botão real “Excluir nota” sob um falso prompt de “Re-autenticação”. Corrigir vulnerabilidades Markdown XSS significa tratar isto como risco central do produto, não casos extremos.

Embeds: a forma mais rápida de permitir execução de script sem querer

Embeds parecem inocentes porque parecem “apenas um vídeo” ou “apenas um tweet”. Na prática, são uma das maneiras mais rápidas de transformar conteúdo do usuário em XSS armazenado, especialmente quando Markdown ou rich-text permitem HTML cru. Muitas vulnerabilidades Markdown XSS começam com uma exceção que alguém fez para iframes.

Se você suporta embeds, decida previamente quais provedores são permitidos e o que “embed” significa no seu app. “Qualquer iframe” não é um recurso. É um buraco de segurança.

Um padrão mais seguro é: usuários colam uma URL normal, seu servidor checa contra uma allowlist e então gera o HTML final de embed. Não aceite tags de iframe fornecidas pelo usuário nem atributos arbitrários como srcdoc, onload ou allow.

Regras que mantêm embeds úteis sem dar superfície de scripting aos atacantes:

  • Permita apenas provedores específicos (por hostname e padrão de caminho), e bloqueie todo o resto.
  • Gere o HTML de embed no servidor a partir de um template limpo, não do HTML do usuário.
  • Desative iframes inline em comentários/notas gerais a menos que haja uma razão forte.
  • Se for necessário permitir iframe, defina limites de tamanho fixos e um sandbox rigoroso.
  • Remova todos os handlers de evento e atributos arriscados; nunca permita URLs javascript:.

Mesmo com sandbox, lembre‑se que embeds carregam conteúdo de terceiros. Trate‑os como uma fronteira separada.

Teste payloads reais de XSS antes de lançar

Rebuild for Production
When the foundation is too shaky, we can rebuild key parts cleanly and prep for deployment.

Sanitizadores muitas vezes parecem bons em uma demonstração rápida, mas falham no input esquisito que usuários reais criam. Antes de liberar comentários ou notas, rode um pequeno conjunto de testes repetíveis que tentem quebrar seu renderizador e suas regras de sanitização.

Comece testando com três papéis e três visualizações. Use um usuário normal que pode postar conteúdo, verifique o que um moderador vê e o que um admin vê. XSS armazenado frequentemente só dispara quando outra pessoa carrega a página, especialmente em dashboards, filas de moderação, previews de e-mail ou painéis de "atividade recente".

Use uma suíte curta de payloads que cubra estilos comuns de bypass (não confie em um ou dois óbvios). Por exemplo, tente caracteres codificados (entidades HTML, atributos com caixa mista), tags malformadas ou não fechadas (para confundir o parser), tags aninhadas (tag externa aparentemente segura, interna perigosa), URLs perigosas dentro de links (javascript: ou data:) e atributos de handlers de evento (como onerror) em qualquer tag que seu sanitizador permita.

Mantenha seus testes realistas: o conteúdo deve continuar a renderizar se for inofensivo, mas nunca deve executar código. Um bom critério é: “Este comentário aparece como texto ou markup seguro, sem popups, redirecionamentos, chamadas de rede inesperadas ou mudanças de UI?”

Também verifique o comportamento em todos os lugares onde o mesmo conteúdo é mostrado. Sanitizar no editor mas não no e-mail de notificação, ou sanitizar na página de comentário mas não na tabela de admin, é um caminho clássico para XSS armazenado.

Erros comuns que causam XSS armazenado

XSS armazenado geralmente acontece quando você assume que o conteúdo do usuário já é “seguro” porque veio de um editor polido. Um WYSIWYG ainda pode emitir HTML perigoso (ou ser enganado para isso), e parsers de Markdown frequentemente permitem casos de borda surpreendentes. Por isso vulnerabilidades Markdown XSS aparecem em produtos que “só suportam comentários”.

Uma armadilha comum é sanitizar apenas no navegador. Limpeza client-side é fácil de contornar com uma requisição direta à sua API ou replay de uma requisição de outro dispositivo. Se o servidor armazenar conteúdo sem sanitizar, você tem um XSS armazenado esperando para disparar em qualquer lugar que esse conteúdo seja exibido.

Outro erro é permitir HTML cru dentro do Markdown para manter recursos funcionando (botões customizados, iframes, estilos sofisticados). Essa escolha transforma silenciosamente seu recurso de Markdown em um serviço de hospedagem de HTML. Mesmo se você remover tags óbvias como \u003cscript\u003e, atacantes podem usar handlers de evento (onerror), URLs sutis ou payloads baseados em SVG dependendo do que você permitir.

Uma grande fonte de incidentes são “renderizadores secundários” que você esquece. Você pode sanitizar a página principal de comentários, mas não a view de admin, o template de email ou o fluxo de exportação para PDF.

Padrões de falha recorrentes incluem tratar saída do editor como dados confiáveis e armazená‑la como está; limpar apenas no cliente e salvar conteúdo cru no servidor; usar sanitizadores diferentes (ou allowlists diferentes) em locais diferentes; renderizar o mesmo conteúdo salvo em HTML, email e ferramentas de admin sem revalidar; e logar ou visualizar HTML cru em dashboards internos.

Exemplo: um usuário posta um “comentário inofensivo” que inclui uma imagem com um atributo criado sob medida. A página pública fica segura, mas o painel de admin usa um renderizador diferente para moderação, e o payload roda quando o staff abre a fila.

Checklist rápido de segurança para comentários e notas

Comentários e notas são onde vulnerabilidades Markdown XSS geralmente aparecem primeiro, porque parecem inofensivos e são lançados rapidamente. Antes de liberá‑los para usuários reais, faça uma checagem rápida com mentalidade de segurança.

Checklist que pega a maioria dos problemas de XSS armazenado:

  • Confirme que HTML cru está totalmente desabilitado no Markdown, ou que é sanitizado depois da renderização. Não confie em “o editor não vai gerar isso”.
  • Use uma allowlist para tags e atributos. Bloqueie todos os handlers de evento como onclick, e evite atributos arriscados como style a menos que você filtre rigorosamente.
  • Valide e normalize URLs em href e src. Rejeite esquemas javascript: e data: (e qualquer outro que você não suporte explicitamente).
  • Tranque os embeds. Se permitir iframes ou recursos de “colar link de vídeo”, defina regras estritas e considere renderizar como links simples em vez de embeds.
  • Verifique todos os lugares onde o conteúdo é mostrado, não apenas a página principal: views de admin, emails de notificação, webviews móveis, exports (PDF/impressão) e dashboards internos.

Depois do checklist, faça um pequeno smoke test com payloads reais. O objetivo não é “ver um alert box”. É confirmar que seu output permanece inerte em todo lugar onde aparece.

Tente alguns inputs conhecidos como ruins (tags script, atributos de handlers e URLs estranhas) e confirme que eles aparecem como texto ou são removidos. Verifique a versão armazenada no banco para garantir que está segura, não apenas a prévia. Repita o teste no lado administrativo, já que admins geralmente veem mais conteúdo e têm privilégios maiores.

Cenário de exemplo: um simples recurso de comentários que vira XSS

Find Hidden Secondary Renderers
We’ll check every place user content renders: dashboards, emails, previews, and exports.

Um fundador lança um pequeno widget de feedback: usuários podem deixar comentários em Markdown em cada página. Parece seguro porque “é só texto”, e a prévia parece correta.

Para suportar rich text, a app converte Markdown para HTML e então renderiza no dashboard de admin. Alguém também adicionou “recursos legais”: auto-link de URLs, suporte a imagens e um embed rápido para vídeos.

Um atacante posta um comentário que parece normal no widget, como um relatório de bug com um link. Mas o Markdown contém HTML que o conversor manteve, ou escondeu um payload num atributo de uma tag permitida. Nada acontece para o atacante. Mais tarde, quando um admin abre o dashboard, o comentário executa código no navegador do admin.

O próximo passo raramente é sutil. O atacante pode roubar a sessão do admin e assumir a conta, ler feedbacks privados ou notas internas mostradas na mesma página e mudar configurações (como webhooks ou chaves de API) usando as permissões do admin.

Um design mais seguro teria parado isso antes do lançamento. Trate comentários como dados não confiáveis e trave o que “rich text” realmente significa: converta Markdown para um subconjunto HTML limitado, sanitize com uma allowlist estrita, remova ou reescreva atributos arriscados (especialmente handlers de evento e certos esquemas de URL), desative embeds por padrão (ou permita apenas um conjunto pequeno de provedores com regras rígidas) e teste payloads reais exatamente na view de admin, não só no widget público.

Próximos passos: lance com segurança e peça uma segunda opinião

Se quer lançar comentários ou notas sem surpresas, trate rich-text como uma feature que precisa de um pequeno plano de segurança, não como um complemento rápido de UI.

Comece escrevendo decisões que você pode manter consistentes pela app: escolha um modelo de conteúdo (texto simples, Markdown sem HTML ou HTML sanitizado), defina elementos e atributos permitidos (seja rígido; a maioria das apps precisa de muito pouco), tranca embeds desde o início (ou pule até ter tempo), crie um pequeno conjunto de payloads XSS que cubra suas features (links, imagens, blocos de código, menções) e decida onde a sanitização acontece (server-side é a fonte da verdade).

Depois adicione um gate de release. O objetivo é simples: nenhum deploy vai ao ar a menos que seus payloads salvos renderizem com segurança na UI real. Isso pega problemas que testes unitários perdem, como um plugin Markdown client-side que habilita HTML sem você notar.

O gate de release pode ser leve. Rode o conjunto de payloads contra fluxos de criação, edição e pré‑visualização. Verifique o output no navegador, não só nas respostas da API. Confirme que as mesmas regras valem em todos os lugares onde o conteúdo aparece (feed, email, views de admin). Adicione um teste de regressão para cada bug encontrado para que se mantenha corrigido.

Se sua app foi gerada ou bastante assistida por ferramentas como Lovable, Bolt, v0, Cursor ou Replit, assuma que padrões podem ser inconsistentes. Uma tela pode usar um renderizador seguro enquanto outra usa uma biblioteca diferente ou um modo de pré‑visualização que permite HTML cru.

Se quiser uma segunda opinião de baixo atrito, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar codebases geradas por IA, incluindo caminhos inseguros de renderização Markdown e rich-text, e pode começar com uma auditoria de código gratuita para apontar riscos de XSS armazenado e problemas relacionados antes do lançamento.

Perguntas Frequentes

Why can Markdown be a security risk if it’s “just text”?

Markdown normalmente é convertido para HTML, e esse HTML é renderizado no navegador de outra pessoa. Se qualquer parte do input sobreviver como HTML executável ou atributos perigosos, pode virar XSS armazenado, mesmo quando a interface do editor parece “apenas texto”.

What type of XSS is most common with comments and notes?

Geralmente é XSS armazenado: um comentário ou nota maliciosa é salva e depois é executada quando outra pessoa a visualiza. Isso é pior que XSS refletido porque pode atingir moderadores, agentes de suporte e clientes muito tempo depois da postagem original.

Where does unsafe HTML usually sneak in with Markdown or rich-text?

O suporte a HTML cru é o maior culpado, mas você também pode ser atingido por links e imagens se não restringir os esquemas de URL. Colar conteúdo de rich-text também pode introduzir tags e atributos inesperados que seu sanitizador não trata como você supõe.

What’s the safest “content model” to choose for user comments?

Prefira Markdown limitado: permita formatação básica e links, e rejeite o resto. Mantenha o conjunto de elementos permitidos pequeno e explícito para evitar hospedar mini páginas web dentro de comentários.

Should I sanitize on save, on render, or both?

Sanitize também na renderização, não só ao salvar. A sanitização em tempo de render ajuda quando seu parser Markdown, a configuração do sanitizador ou o comportamento do navegador mudam e conteúdos antigos ficam perigosos.

How do I make Markdown links safe against `javascript:` tricks?

Permita apenas esquemas seguros como https: (e opcionalmente http: em casos controlados) após decodificar e normalizar. Bloqueie javascript:, data:, file: e outros esquemas inesperados, pois são formas comuns de disfarçar execução ou comportamento esquisito em links “normais” do Markdown.

Are images in Markdown dangerous, or just annoying?

Se precisar suportar imagens, trate src como não confiável: restrinja esquemas e considere fazer proxy das imagens para que o navegador não busque diretamente de servidores controlados por atacantes. Se imagens não são necessárias nos comentários, a escolha mais segura é desativá-las.

What’s the safest way to support embeds (videos, tweets, etc.)?

Não aceite HTML de iframe arbitrário dos usuários. Uma abordagem mais segura é permitir que o usuário cole uma URL normal e, no servidor, gerar um snippet de embed fixo apenas para provedores permitidos.

What’s one mistake teams make when they “sanitize” rich-text?

Sanitize no servidor, não apenas no navegador. Filtragem client-side é fácil de contornar chamando sua API diretamente, o que pode deixar conteúdo não sanitizado armazenado e mais tarde renderizado em views administrativas ou emails.

How can I quickly test if my Markdown rendering is vulnerable to XSS?

Teste um pequeno conjunto de payloads realistas em todos os lugares onde o conteúdo aparece: página pública, telas de admin/moderação, notificações e previews. O objetivo é que a entrada permaneça inerte em todos os lugares — sem popups, redirecionamentos, mudanças inesperadas de UI ou chamadas de rede ocultas.