flawopen.com/xss/Javascript
XSS en JavaScript: protege DOM, React y Express
Los destinos DOM/React inseguros y los enlaces pueden ejecutar JavaScript. Usa API de texto, sanea HTML, valida URL y aplica Trusted Types (CWE-79).
Imagina un panel de indicaciones teatrales. Si el regidor trata cada nota del público como una instrucción escénica, una nota puede añadir una escena inesperada; si la muestra como diálogo normal, sigue siendo texto. En el navegador, innerHTML y dangerouslySetInnerHTML de React piden al DOM que interprete el texto como marcado, mientras textContent lo muestra literalmente. Escapa el texto normal, sanea el poco HTML que permitas y valida los esquemas de URL antes de crear enlaces.
Conceptos Clave y Términos
innerHTML vs textContent- Asignar a
innerHTML,outerHTMLoinsertAdjacentHTMLhace que el navegador interprete la cadena como HTML, así que un<img onerror>en ella se convierte en un elemento activo.textContentycreateTextNodeinsertan solo texto; nada del valor se interpreta como marcado. Template literals in res.send()res.send()de Express envía las cadenas comotext/htmlpor defecto. Un template literal evalúa${req.query.q}antes del envío, así que la respuesta contiene lo que el usuario puso en la query string.req.query.qtambién puede ser un array o un objeto, así que comprueba el tipo.dangerouslySetInnerHTML + DOMPurify- React escapa el texto en JSX automáticamente;
dangerouslySetInnerHTMLes la salida explícita. Pasa el valor porDOMPurify.sanitize(), que lo analiza con el propio parser del navegador y elimina scripts, manejadores de eventos y URLs peligrosas antes de que React lo inserte. javascript: URLs in href- JSX escapa el valor del atributo, pero
href="javascript:..."es una URL válida que ejecuta código al hacer clic. Analízala connew URL()y permite solohttp:yhttps:; el parser de URLs elimina espacios y tabulaciones iniciales que las comprobaciones simples de prefijo pasan por alto. Trusted Types- Con la directiva CSP
require-trusted-types-for 'script', los navegadores compatibles rechazan cadenas simples en sinks comoinnerHTMLy solo aceptan objetosTrustedHTMLcreados por una política que tú defines, normalmente una que llama a DOMPurify. Los sinks nuevos no pueden colarse sin que se note.
Mapeos de marcos
- Referencia CWE para la debilidad descrita en esta guía: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Flujo de Ataque Paso a Paso
La entrada llega a un sink
Una búsqueda, un campo de perfil cargado desde una API o un enlace guardado por otro usuario llega como cadena y se pasa a innerHTML, dangerouslySetInnerHTML, href o res.send().
El valor se convierte en marcado
Con el valor de prueba inofensivo <img src=x onerror=alert(1)>, innerHTML y res.send() producen un elemento <img> real; un enlace guardado javascript:alert(1) se vuelve un script clicable.
El navegador lo ejecuta
La imagen no carga y dispara onerror, o el usuario hace clic en el enlace. El código se ejecuta de inmediato, sin ninguna otra acción del atacante.
El script actúa como el sitio
Se ejecuta con el origen de la página: puede leer el DOM, llamar a las APIs del sitio con las cookies de la víctima o los tokens de localStorage, y cambiar lo que ve el usuario.
Código Fuente: Vulnerable vs. Seguro
// profile.jsx: cada función de abajo deja que una cadena se interprete como HTML
import express from 'express';
const app = express();
// Express: el valor de la query entra en el HTML antes del envío
app.get('/search', (req, res) => {
res.send(`<h1>Results for ${req.query.q}</h1>`);
});
// innerHTML interpreta name como marcado
export function showGreeting(el, name) {
el.innerHTML = `Hello, ${name}`;
}
// React inserta el HTML tal como llega
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}
// una URL javascript: se ejecuta al hacer clic en el enlace
export function Website({ url }) {
return <a href={url}>Website</a>;
}
// profile.jsx: el texto sigue siendo texto; HTML y URLs se comprueban antes
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';
const app = express();
// solo se aceptan cadenas, y se escapan como 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 marcado
export function showGreeting(el, name) {
el.textContent = `Hello, ${name}`;
}
// DOMPurify elimina scripts y manejadores de eventos antes de insertar
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// solo sobreviven URLs http y https; lo demás se convierte en #
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 Verificación de Seguridad para Ingeniería
- ✓Activa
react/no-dangeryeslint-plugin-no-unsanitizeden la CI, y bloqueainnerHTML,outerHTML,insertAdjacentHTMLydocument.writecon valores no literales. - ✓Usa
textContentpara texto; cualquier HTML que deba mostrarse pasa porDOMPurify.sanitize()en el punto de inserción. - ✓Valida toda URL aportada por el usuario con
new URL()y una lista permitidahttp:/https:antes de que llegue ahref,srcowindow.location. - ✓En Express, renderiza con un motor de vistas que escape automáticamente o con
escape-html, y rechaza los parámetros de query que no sean cadenas. - ✓Envía una CSP con
script-srcbasado en nonce, sin'unsafe-inline'y conrequire-trusted-types-for 'script'; guarda los tokens de sesión en cookiesHttpOnlyy no enlocalStorage.