FlawOpen

flawopen.com/xss/Javascript

● CWE-79 · Alta
Pesquisa · FlawOpen

XSS em JavaScript: proteja DOM, React e Express

Alvos DOM/React inseguros e links podem executar JavaScript. Prefira APIs de texto, sanitize HTML, valide URLs e aplique Trusted Types (CWE-79).

💡 Explicação em Linguagem Simples (ELI5)

Imagine um painel de deixas de teatro. Se o contrarregra tratar cada bilhete de visitante como instrução de palco, um bilhete pode acrescentar uma cena inesperada; se for exibido como fala comum, continua sendo texto. No navegador, innerHTML e dangerouslySetInnerHTML do React pedem ao DOM que interprete o texto como marcação, enquanto textContent o mostra literalmente. Escape texto normal, sanitize o pouco HTML que você permitir e valide os esquemas de URL antes de criar links.

Conceitos Centrais e Termos

innerHTML vs textContent
Atribuir a innerHTML, outerHTML ou insertAdjacentHTML faz o navegador interpretar a string como HTML, então um <img onerror> dentro dela vira um elemento ativo. textContent e createTextNode inserem só texto; nada no valor é interpretado como marcação.
Template literals in res.send()
O res.send() do Express envia strings como text/html por padrão. Um template literal avalia ${req.query.q} antes do envio, então a resposta contém o que o usuário pôs na query string. req.query.q também pode ser array ou objeto, então verifique o tipo.
dangerouslySetInnerHTML + DOMPurify
O React escapa texto em JSX automaticamente; dangerouslySetInnerHTML é a saída explícita. Passe o valor por DOMPurify.sanitize(), que o interpreta com o próprio parser do navegador e remove scripts, manipuladores de eventos e URLs perigosas antes de o React inseri-lo.
javascript: URLs in href
O JSX escapa o valor do atributo, mas href="javascript:..." é uma URL válida que roda código ao ser clicada. Interprete com new URL() e permita só http: e https:; o parser de URL remove espaços e tabulações iniciais que checagens simples de prefixo deixam passar.
Trusted Types
Com a diretiva de CSP require-trusted-types-for 'script', navegadores compatíveis recusam strings simples em sinks como innerHTML e aceitam só objetos TrustedHTML criados por uma política definida por você, normalmente uma que chama o DOMPurify. Novos sinks não passam despercebidos.

Mapeamentos de frameworks

Fluxo de Ataque Passo a Paso

Passo 1

A entrada chega a um sink

Uma busca, um campo de perfil vindo de uma API ou um link salvo por outro usuário chega como string e é passado a innerHTML, dangerouslySetInnerHTML, href ou res.send().

Passo 2

O valor vira marcação

Com o valor de teste inofensivo <img src=x onerror=alert(1)>, innerHTML e res.send() produzem um elemento <img> real; um link salvo javascript:alert(1) vira um script clicável.

Passo 3

O navegador o executa

A imagem não carrega e dispara onerror, ou o usuário clica no link. O código roda na hora, sem nenhuma outra ação do atacante.

Passo 4

O script age como o site

Ele roda com a origem da página: pode ler o DOM, chamar as APIs do site com os cookies da vítima ou tokens do localStorage e mudar o que o usuário vê.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
// profile.jsx: cada função abaixo deixa uma string ser interpretada como HTML
import express from 'express';

const app = express();

// Express: o valor da query entra no HTML antes do envio
app.get('/search', (req, res) => {
  res.send(`<h1>Results for ${req.query.q}</h1>`);
});

// innerHTML interpreta name como marcação
export function showGreeting(el, name) {
  el.innerHTML = `Hello, ${name}`;
}

// o React insere o HTML exatamente como veio
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}

// uma URL javascript: roda quando o link é clicado
export function Website({ url }) {
  return <a href={url}>Website</a>;
}
PATCH SEGURO E ROBUSTO
// profile.jsx: texto continua texto; HTML e URLs são verificados antes
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';

const app = express();

// só strings são aceitas, e com escape de HTML
app.get('/search', (req, res) => {
  const q = typeof req.query.q === 'string' ? req.query.q : '';
  res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});

// textContent nunca interpreta marcação
export function showGreeting(el, name) {
  el.textContent = `Hello, ${name}`;
}

// o DOMPurify remove scripts e manipuladores de eventos antes da inserção
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

// só URLs http e https passam; o resto vira #
function safeHref(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
  } catch {
    return '#';
  }
}

export function Website({ url }) {
  return <a href={safeHref(url)}>Website</a>;
}

Lista de Verificação de Segurança para Engenharia

Fontes