flawopen.com/xss/Javascript
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).
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,outerHTMLouinsertAdjacentHTMLfaz o navegador interpretar a string como HTML, então um<img onerror>dentro dela vira um elemento ativo.textContentecreateTextNodeinserem só texto; nada no valor é interpretado como marcação. Template literals in res.send()- O
res.send()do Express envia strings comotext/htmlpor 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.qtambé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 porDOMPurify.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 comnew URL()e permita sóhttp:ehttps:; 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 comoinnerHTMLe aceitam só objetosTrustedHTMLcriados por uma política definida por você, normalmente uma que chama o DOMPurify. Novos sinks não passam despercebidos.
Mapeamentos de frameworks
- Referência CWE para a fraqueza descrita neste guia: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Fluxo de Ataque Passo a Paso
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().
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.
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.
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
// 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>;
}
// 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
- ✓Ative
react/no-dangereeslint-plugin-no-unsanitizedna CI e bloqueieinnerHTML,outerHTML,insertAdjacentHTMLedocument.writecom valores não literais. - ✓Use
textContentpara texto; todo HTML que precise ser renderizado deve passar porDOMPurify.sanitize()no ponto de inserção. - ✓Valide toda URL fornecida pelo usuário com
new URL()e uma allowlisthttp:/https:antes que chegue ahref,srcouwindow.location. - ✓No Express, renderize com um view engine que escape automaticamente ou com
escape-html, e rejeite parâmetros de query que não sejam string. - ✓Envie uma CSP com
script-srcbaseado em nonce, sem'unsafe-inline'e comrequire-trusted-types-for 'script'; guarde tokens de sessão em cookiesHttpOnly, não nolocalStorage.