JSON Toolkit

Como o XSS acontece através de JSON

Resumo: JSON nunca executa sozinho. O XSS acontece no momento em que você o trata como HTML — concatenação de strings no servidor, innerHTML no frontend ou inclusão de JSON direto em uma tag script.

Intermediário~4 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Três formas pelas quais JSON se torna XSS e como preveni-las

Resumo da ópera: JSON nunca executa sozinho — feche primeiro as três rotas de saída: faça escape no servidor conforme o contexto, use textContent em vez de innerHTML e substitua < por \u003c ao embutir JSON.

{"name": "<img src=x onerror=alert(1)>"} é dado válido. Se é perigoso depende inteiramente de onde você o coloca.

Porta um: montagem de strings no servidor

res.send(`<div>Hello, ${data.name}</div>`);

O navegador analisa as tags dentro de data.name. O JSON aqui é apenas o contêiner; a falha é escapar para o contexto errado.

Correção: use o escape padrão de um engine de templates (<%= %> em EJS, {{ }} em Vue e Django). Evite <%- %>, v-html e |safe para qualquer coisa controlada pelo usuário.

Porta dois: innerHTML

el.innerHTML = user.bio;    // analisa marcação
el.textContent = user.bio;  // insere texto

O {user.bio} do React e o {{ user.bio }} do Vue criam nós de texto e são seguros por padrão. dangerouslySetInnerHTML e v-html são opt-outs explícitos.

Se você realmente precisa de rich text, sanitize com uma lista de permissões (DOMPurify e similares) em vez de confiar no backend.

Porta três: inclusão em uma tag script

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

Um </script> dentro dos dados fecha o bloco antes do fim e tudo depois disso roda como um novo script. Este é o caso mais esquecido.

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

Ainda é JSON válido, idêntico após a análise, e não consegue mais fechar a tag. Next.js e Nuxt fazem o mesmo para __NEXT_DATA__ e __NUXT__.

Dois vizinhos que vale conhecer

JSON hijacking — navegadores antigos executavam JSON carregado via <script src>; redefinir Array vazava o conteúdo. Navegadores modernos corrigiram isso, e Content-Type: application/json mais X-Content-Type-Options: nosniff fecham o resto.

Resquícios de JSONP — um nome de callback controlado pelo usuário é execução de código arbitrário por design. Não o use em sistemas novos.

Lista de verificação

  • Todo lugar em que um campo JSON chega a HTML é escapado conforme o contexto (HTML, atributo, JS, URL)
  • Nenhum innerHTML, v-html ou dangerouslySetInnerHTML consumindo strings de API
  • JSON embutido escapa <, > e &
  • Rich text passa por sanitização com lista de permissões, não por filtro de lista negra
  • As respostas trazem Content-Type: application/json e nosniff

Perguntas frequentes

Por que JSON válido ainda pode disparar XSS?

O JSON é apenas texto e nunca executa; ser válido só significa que ele é analisável. O perigo aparece quando ele é escrito em um contexto HTML ou de script: o navegador analisa as tags, ou um </script> fecha o bloco antes do fim e o restante roda como código. A falha é a ausência de escape sensível ao contexto na saída, não o formato dos dados.

Devo usar textContent ou innerHTML para renderizar campos de API?

Prefira textContent: ele insere o valor como texto simples, então as tags são exibidas literalmente. innerHTML analisa marcação, então um campo de API contendo <img onerror=...> executa. O {user.bio} do React e o {{ user.bio }} do Vue criam nós de texto por padrão; dangerouslySetInnerHTML e v-html são opt-outs explícitos.

Como devo escapar JSON embutido em uma tag <script>?

Sim. Um </script> dentro dos dados fecha o bloco antes do fim e tudo depois disso roda como um novo script. Após serializar, substitua <, > e & por \u003c, \u003e e \u0026: o resultado ainda é JSON válido, é analisado de forma idêntica e não consegue mais fechar a tag.

Leitura adicional