Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 073 — XSS y escapado#

⬅️ 072 · 📚 Parte 5 · 🎓 Clases · 074 ➡️ Parte 5 — Identidad y seguridad · Nivel 🟡 intermedio · Pista frontendClase construida — 5 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Ver qué escapa el framework por omisión y qué no. El Cross-Site Scripting es texto de un usuario que el navegador ejecuta como código; la defensa moderna no es filtrar lo malo sino escapar todo por omisión — y dejar la vía insegura como una puerta explícita con nombre de peligro [owasp-cheatsheets].

🧩 La situación#

Un usuario escribió <script>alerta(1)</script> en un campo de texto. Cada framework lo renderiza dos veces: por su interpolación normal —donde el script tiene que llegar neutralizado— y por su puerta explícita — donde llega vivo, porque alguien lo pidió con todas las letras.

🧮 El contrato#

Petición Respuesta Qué mide
GET /seguro contiene &lt;script y no contiene <script> la omisión escapa
GET /seguro contiene alerta(1) escapar neutraliza, no recorta: el texto sigue ahí
GET /inseguro contiene <script>alerta(1)</script> literal la puerta explícita no escapa

El segundo caso separa el escapado del filtrado: un framework que borrara el script también pasaría el primer caso — y estaría destruyendo datos del usuario. La respuesta correcta conserva cada carácter y le quita el poder.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
XSS (Cross-Site Scripting) Texto de un usuario que el navegador acaba ejecutando como código. La defensa moderna no es filtrar lo malo: es escapar todo por omisión y dejar la vía insegura como una puerta explícita con nombre de peligro.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
React biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2013 MIT Meta y colaboradores
Vue framework de interfaz de JavaScript/TypeScript (JavaScript) 2014 MIT proyecto independiente
Svelte framework de interfaz de JavaScript/TypeScript (JavaScript) 2016 MIT proyecto independiente
SolidJS biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2018 MIT proyecto independiente
Lit web-components-library de JavaScript/TypeScript (TypeScript) 2021 BSD-3-Clause Google

🔧 React#

Impuso la idea de la interfaz como función del estado y el árbol virtual. Es una biblioteca, no un framework: no arranca tu aplicación ni define su ciclo de vida.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Vue#

Adopción progresiva: sirve como etiqueta en una página existente o como framework completo. Su reactividad fina influyó en toda la generación siguiente.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Svelte#

Mueve el trabajo del navegador al compilador. Cambia el coste de ejecución por dependencia de la fase de construcción: un compromiso, no una mejora gratuita.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Inseguro.svelte componente de Svelte
Seguro.svelte componente de Svelte
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 SolidJS#

Reactividad de grano fino sin árbol virtual: el componente se ejecuta una vez y solo se actualiza lo que leyó el valor cambiado.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec babel App.jsx --out-file App.compilada.mjs

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
.babelrc configuración de Babel: qué transformación se aplica al compilar
App.jsx componente en JSX
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Lit#

Capa mínima sobre los componentes web del propio estándar. La apuesta por la plataforma en lugar de por el framework.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Para que el resultado sea medible sin navegador, cada implementación usa el renderizado de servidor real de su framework: react-dom/server, @vue/server-renderer, el compilador de Svelte con generate: "server", el preset de Babel de Solid con generate: "ssr" y @lit-labs/ssr. No es una imitación de las reglas de escapado — es el mismo código del framework, y lo que produce aquí es lo que produce en el DOM.

Las cinco renderizan exactamente el mismo texto hostil:

const TEXTO = "<script>alerta(1)</script>";

Y cada una lo pinta dos veces: por la vía normal y por la puerta explícita. Lo que hay que mirar es cómo se llama la puerta.

React · react/server.mjs#

  "/seguro": () => renderToStaticMarkup(h("p", null, TEXTO)),
  "/inseguro": () =>
    renderToStaticMarkup(h("div", { dangerouslySetInnerHTML: { __html: TEXTO } })),

La vía segura no tiene nombre: es la interpolación normal, y no existe opción para olvidarse de escapar. La insegura tiene el nombre más honesto de la industria — dangerouslySetInnerHTML — y encima obliga a envolver el valor en un objeto con la clave __html. Es imposible teclearla por accidente y difícil de teclear sin enterarse.

Vue · vue/server.mjs#

  "/seguro": () => createSSRApp({ render: () => h("p", TEXTO) }),
  "/inseguro": () => createSSRApp({ render: () => h("div", { innerHTML: TEXTO }) }),

Se usan funciones de render porque es exactamente a lo que compilan las plantillas: {{ texto }} compila a un hijo de texto escapado, y v-html compila a la propiedad innerHTML. Ver el destino de la compilación es más informativo que ver la plantilla: deja claro que v-html no es una directiva mágica, es una asignación a innerHTML con otro nombre.

Svelte · svelte/Seguro.svelte e Inseguro.svelte#

<p>{texto}</p>
<div>{@html texto}</div>

La diferencia son cuatro caracteres: @html. Es la puerta más barata de teclear del elenco, y la documentación de Svelte lo compensa abriendo su descripción con la advertencia de XSS. Merece la pena verlo junto a React: el mismo poder, dos costes de escritura muy distintos.

Solid · solid/App.jsx#

export const seguro = (texto) => renderToString(() => <p>{texto}</p>);
export const inseguro = (texto) => renderToString(() => <div innerHTML={texto} />);

JSX igual que React, y la puerta sin disfraz: la propiedad innerHTML tal cual. Solid no le pone un nombre alarmante porque no le pone ningún nombre — usa el de la plataforma. Es coherente con su filosofía (compilar a operaciones del DOM, no interponer un modelo propio) y a la vez es la puerta menos señalada de las cinco.

Lit · lit/server.mjs#

  "/seguro": () => collectResultSync(render(html`<p>${TEXTO}</p>`)),
  "/inseguro": () => collectResultSync(render(html`<div>${unsafeHTML(TEXTO)}</div>`)),

Aquí el escapado no lo hace un framework: lo hace la plantilla etiquetada. La función `html` recibe las partes estáticas y las interpolaciones por separado, así que sabe con certeza qué escribió el programador y qué vino de fuera — la misma distinción estructural que hace segura una consulta parametrizada en la clase 074.

Y la puerta es una directiva importada: unsafeHTML hay que traerla de su módulo. Una búsqueda de unsafe-html en el proyecto encuentra todos los sitios donde alguien abrió la puerta, que es más de lo que puede decirse de innerHTML.

📊 Comparación: las puertas, por nombre#

Framework Interpolación segura La puerta explícita
React {texto} dangerouslySetInnerHTML={{ __html }}
Vue {{ texto }} v-html
Svelte {texto} {@html texto}
Solid {texto} innerHTML={texto}
Lit ${texto} unsafeHTML(texto)

La columna derecha es una lección de diseño de API: React eligió el nombre más incómodo de la industria y esa incomodidad es la característica — nadie teclea dangerously sin enterarse. v-html y innerHTML son más neutros, y sus documentaciones compensan abriendo con la advertencia.

✂️ Los tres que salieron del elenco#

El manifiesto planteaba ocho frameworks; tres no pueden ejercitar de verdad lo que esta clase mide, y antes que simularlos, se recortan:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 073

Los casos están en contrato.json. El verificador ejecuta las implementaciones que encuentre y declara las que omitió.

🧪 Reto de transferencia#

Añade a cada implementación una ruta /atributo que interpole el texto malicioso " onmouseover="alerta(1) dentro de un atributo (title), y comprueba con el contrato que el atributo no se rompe. Es el segundo contexto de escapado — y el primero que las plantillas caseras con replace('<', '&lt;') pierden.

🔗 Enlaces#

Fuentes#