Análisis del examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5, tarde, pregunta 1 — el XSS almacenado donde 16 reseñas se muestran como solo 2
· Actualizado el: · Go Komura · Especialista en Seguridad de la Información Registrado, Examen SC, XSS, Cross-Site Scripting, Aplicación web, Seguridad de la información, Vulnerabilidad, IPA, Gestión de sesiones
«Deberían aparecer 16 reseñas, pero solo se muestran 2.»
La pregunta 1 de la tarde del examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5 (2023), arranca con esta consulta de un usuario1. No hay nada raro en la navegación por las pantallas ni aparece ningún error. Simplemente, el número mostrado no coincide.
La causa fue un cross-site scripting (XSS) almacenado. Pero lo interesante de este problema no es la respuesta «XSS» en sí misma. Lo interesante es que todas las «contramedidas que parecían razonables» que ya tenía la empresa Q (el comercio electrónico que aparece en el enunciado) fueron burladas, una tras otra. El límite de 50 caracteres del título de reseña fue sorteado, el token necesario para la carga se obtuvo por el procedimiento legítimo, y el ID de sesión robado salió sin ser enviado nunca a un servidor externo.
En este artículo recorremos esta pregunta apartado por apartado, ordenando dónde se separan las contramedidas que no funcionaron de las que sí habrían funcionado.
Lo que obtendrá de este artículo, además de la resolución del examen (el modelo de respuesta de cada pregunta y su fundamento), es una visión completa y práctica de las contramedidas contra el XSS. Está escrito de modo que quien lea para preparar el examen pueda seguir la sección correspondiente a cada pregunta, y quien solo necesite la perspectiva de revisión de código para el trabajo diario pueda leer directamente los capítulos 9 y 10 sin perder el hilo.
Para una visión de conjunto de con qué criterios revisar la seguridad de un sitio web completo, véase Usar «Cómo crear un sitio web seguro» de IPA como lista de verificación. Este artículo profundiza, con un caso concreto, en una de las 11 vulnerabilidades que se tratan allí: el XSS.
1. Primero, la conclusión
- El tipo de vulnerabilidad es XSS almacenado. La cadena del atacante quedó guardada en el servidor y, desde entonces, se emitía en el HTML de todas las personas que abrían la página. Que el script incrustado use APIs del DOM es una cuestión aparte de si se trata de DOM Based XSS
- El límite de caracteres de entrada no funcionó como contramedida de XSS. El atacante dividió la publicación en 15 partes y logró que el HTML intercalado entre publicaciones se saltara como comentario de JavaScript, enlazando todo en un único script
- El token de carga (contramedida de CSRF) tampoco funcionó como contramedida de XSS. El script de ataque obtiene el token siguiendo el mismo procedimiento que la pantalla legítima. Mientras se ejecute en el mismo origen, puede hacer todo lo que puede hacer un usuario legítimo
- El ID de sesión robado no se envió al exterior. El contenido de la cookie se colocó como archivo de imagen «a.png» en la propia función de carga de icono del sitio, y el atacante lo recuperó simplemente viéndolo. Las contramedidas de salida (egress) no pueden detectarlo
- El enunciado señala explícitamente 3 elementos que le faltaban a la empresa Q: el escapado en la salida (solución fundamental), y el atributo HttpOnly de la cookie y la verificación del formato del archivo cargado (estas dos, contramedidas complementarias). Con cualquiera de ellas habría bastado para romper la cadena del ataque en algún punto. Sin embargo, la verificación de formato por sí sola puede sortearse cambiando de técnica, por lo que hace falta llegar hasta el reprocesado
- Aunque no aparece en el enunciado, la directiva
script-srcde la CSP también puede cortar la misma cadena. Con una configuración que no permita'unsafe-inline', el script en línea incrustado directamente no llegaría a ejecutarse
2. Sobre el material de partida — la fuente y cómo se trata en este artículo
El problema tratado es el siguiente.
Fuente: Examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5 (2023), tarde, pregunta 1
IPA indica que, salvo que una ley lo establezca expresamente, no se necesita autorización ni pago alguno para usar sus exámenes anteriores publicados. Ahora bien, esto no significa que IPA renuncie a los derechos de autor: exige que se indique la fuente en el formato «año fiscal, convocatoria, categoría de examen, franja horaria, número de pregunta, etc.», y que se señale también, cuando corresponda, que una pregunta ha sido modificada2.
Este artículo no reproduce tal cual el HTML ni los scripts publicados en el cuadernillo del examen. En la medida necesaria para explicar el mecanismo, se sustituyen por código de ejemplo equivalente escrito por nuestra empresa. El texto de los enunciados y los modelos de respuesta también se tratan de forma resumida. El cuadernillo original, los modelos de respuesta y el comentario de calificación pueden descargarse gratis desde la web de IPA, así que recomendamos tenerlos abiertos mientras lee1 3 4.
Correspondencia entre las preguntas y este artículo
Para quien lea con el cuadernillo abierto, aquí se muestra a qué sección de este artículo corresponde cada pregunta. Puede empezar a leer por la pregunta que quiera resolver.
| Pregunta | Qué se pregunta (límite de caracteres) | Sección correspondiente de este artículo |
|---|---|---|
| Pregunta 1(1) | Tipo de vulnerabilidad XSS explotada (opción múltiple, 3 opciones) | Capítulo 4 |
| Pregunta 1(2) | Contramedida en la aplicación web Q (30 caracteres o menos) | Capítulo 4, «Pregunta 1(2): la contramedida» |
| Pregunta 2 | Método para lograr que se ejecutara un script más largo que el límite de caracteres de entrada (50 caracteres o menos) | Capítulo 5 |
| Pregunta 3(1) | Qué hacen las líneas 6 a 20 del script de ataque (60 caracteres o menos) | Capítulo 6 |
| Pregunta 3(2) | Cómo obtiene el atacante la información cargada (50 caracteres o menos) | Capítulo 7 |
| Pregunta 3(3) | Qué se puede hacer con la información obtenida (40 caracteres o menos) | Capítulo 7, «Pregunta 3(3): qué se puede hacer con el ID de sesión» |
| Pregunta 4 | Mecanismo del navegador por el que el ataque no tiene éxito desde el dominio del atacante (40 caracteres o menos) | Capítulo 8 |
Si solo necesita la parte práctica, sin las preguntas del examen, lea directamente los capítulos 9 (lista de contramedidas que funcionaron y las que no) y 10 (puntos de revisión de código). Si quiere ver de un vistazo toda la cadena del ataque, el diagrama de secuencia del capítulo 7 muestra el panorama completo desde la publicación hasta la recuperación.
Correspondencia entre el texto del cuadernillo y los ejemplos de este artículo
Para que pueda contrastarlo con el texto original, aquí se resume qué se sustituyó y cómo.
| Descripción del cuadernillo | Tratamiento en este artículo | Dónde aparece |
|---|---|---|
| El HTML de la página V (con los títulos de reseña publicados de forma fragmentada) | No se reproduce tal cual; nuestra empresa escribió un ejemplo equivalente que condensa el mismo mecanismo en 3 publicaciones | Capítulo 5 |
| El script de ataque extraído (unas 20 líneas) | No se reproduce tal cual; nuestra empresa escribió JavaScript equivalente que realiza el mismo procesamiento. Los comentarios en el código son explicaciones propias de este artículo | Capítulo 6 |
| El texto de cada pregunta | Resumen que conserva el sentido (las condiciones, como los límites de caracteres, mantienen el valor original) | Inicio de cada uno de los capítulos 4 a 8 |
| Modelos de respuesta | Los modelos de respuesta publicados por IPA3 | A lo largo de los capítulos 4 a 8 |
| Comentario de calificación | El fragmento correspondiente del comentario de calificación publicado por IPA4 | Capítulos 4, 5 y 7 |
| Especificación del enunciado (empresa Q, página V, socios A/B, funciones y límites de caracteres, etc.) | Resumen fiel al texto original | Capítulo 2, «El escenario del problema» |
El escenario del problema
Los protagonistas son la empresa Q, una empresa de comercio electrónico de ropa con 100 empleados. Opera su sitio de comercio electrónico con un sistema desarrollado internamente llamado «aplicación web Q», y los usuarios acceden por HTTPS. El escenario es que la empresa Q acaba de añadir una función de reseñas de productos para socios.
Hay 5 puntos de la especificación que conviene tener presentes.
| Función | Especificación |
|---|---|
| Inicio de sesión | Autentica con ID de socio y contraseña, y emite el ID de sesión como cookie |
| Reseña de producto | Solo pueden publicar los socios con sesión iniciada. El título de la reseña tiene un límite de 50 caracteres y el detalle de la reseña de 300 caracteres, ambos en texto libre |
| Perfil del socio | Ofrece una página para cargar una imagen de icono y otra para registrar los datos de la tarjeta de crédito. Ambas están disponibles solo para socios con sesión iniciada |
| Carga de la imagen de icono | Envía un archivo de imagen y un token como parámetros a /user/upload. Solo tiene éxito cuando el token coincide con el emitido en /user/profile |
| Visualización de la imagen de icono | La imagen de icono cargada se muestra en la página de configuración del perfil del socio y en las páginas de reseñas |
Las últimas 2 filas resultarán relevantes más adelante.
3. El síntoma — por qué 16 se convirtieron en 2
Llega una consulta de un socio: «En la página de reseñas de una camiseta lisa (página V) deberían mostrarse 16 reseñas, pero solo se ven 2». Cuando N, del departamento de desarrollo, abre la página V, la pantalla muestra lo siguiente:
- El encabezado dice «16 reseñas»
- 1 reseña del socio A (título «Good», cuerpo «Nice shirt!»)
- 1 reseña del socio B
- Al final, «Eso es todo, 16 reseñas en total»
El número mostrado es 16, pero lo que realmente aparece son 2. Al comprobar el HTML en ese momento se descubre que existen 15 publicaciones del socio A, y que en ellas hay incrustado un script largo.
Es decir, el contenido de las 16 reseñas sí se emite en el HTML. Que solo se vean 2 se debe a que la mayor parte queda dentro de un elemento <script>, y el navegador deja de tratarla como contenido que se debe mostrar.
El tramo que queda absorbido, con precisión, no es «los 15 completos». La etiqueta de apertura <script> aparece a mitad del título de la primera reseña (justo después de «Good»), y la etiqueta de cierre </script> aparece al final del título de la reseña 15. Por lo tanto:
- La tarjeta de la primera reseña (icono, nombre visible, fecha, estrellas y el título hasta «Good») está antes de
<script>, así que se muestra - Desde ahí hasta el final del título de la reseña 15 queda absorbido como contenido del elemento script
- El resto de la reseña 15 (el cuerpo «Nice shirt!») está después de
</script>, así que se muestra
Como resultado, la cabecera de la primera publicación se une con el cuerpo de la reseña 15, y en pantalla se ve como «una única reseña del socio A con título Good y cuerpo Nice shirt!». Sumando la reseña del socio B, quedan 2. Que el atacante colocara la cadena natural «Good» al principio de la primera publicación y «Nice shirt!» en el cuerpo de la reseña 15 probablemente fue para que la rotura de la visualización no llamara la atención.
Esta lectura del síntoma —«el número coincide pero lo mostrado no»— también es útil en el trabajo diario. Es la puerta de entrada para sospechar que no hay un fallo en la lógica del contador, sino que la estructura del HTML emitido está rota.
4. Por qué es XSS «almacenado» — pregunta 1
La pregunta 1 pide elegir el tipo de vulnerabilidad XSS usada en este ataque entre 3 opciones: DOM Based XSS, XSS almacenado y XSS reflejado. La respuesta correcta es XSS almacenado.
Sobre esta pregunta, el comentario de calificación de IPA dice lo siguiente.
La tasa de aciertos fue media, pero se observaron algunas respuestas erróneas de “DOM Based XSS”, posiblemente porque el script usaba el DOM.
El script de ataque del enunciado usa XMLHttpRequest, recibe la respuesta como DOM y obtiene elementos con getElementById. Efectivamente, toca el DOM. Pero eso no tiene relación con el tipo de vulnerabilidad.
Lo que marca la diferencia es «dónde se convierte la cadena de ataque en script»
Lo que distingue los 3 tipos es dónde la cadena de ataque se convierte en código ejecutable, es decir, si el punto de salida vulnerable (el sumidero) está en el servidor o en el navegador.
| Tipo | Sumidero (dónde la cadena de ataque se vuelve ejecutable) | De dónde viene la cadena de ataque |
|---|---|---|
| XSS reflejado | El proceso del servidor que construye el HTML | La propia solicitud, por ejemplo un parámetro de una URL preparada por el atacante |
| XSS almacenado | El proceso del servidor que construye el HTML | Datos guardados en el servidor |
| DOM Based XSS | JavaScript del lado del navegador (asignación a innerHTML, eval, document.write, etc.) |
El fragmento de la URL, postMessage, valores recibidos del servidor, etc. |
En este problema, el servidor incrustaba directamente en el HTML el título de reseña que había publicado el atacante y lo devolvía tal cual. El sumidero está en el proceso de salida del servidor, y esa cadena se guardaba en la base de datos y desde entonces se repartía a todas las personas que abrían la página V. Por lo tanto, es almacenado.
flowchart TD
A["La cadena del atacante<br/>se ejecutó como script"] --> B{"¿Dónde se volvió<br/>ejecutable?"}
B -->|"JS del navegador la pasó<br/>a innerHTML u otro<br/>sumidero similar"| C["DOM Based XSS"]
B -->|"Estaba en el HTML<br/>que construyó el<br/>servidor"| D{"¿Esa cadena está<br/>guardada en el<br/>servidor?"}
D -->|"Sí, guardada<br/>(se emite a todos<br/>desde entonces)"| E["XSS almacenado"]
D -->|"No, no guardada<br/>(solo para esa<br/>solicitud)"| F["XSS reflejado"]
Ahora bien, simplificar diciendo «si la cadena de ataque aparece en la respuesta del servidor es almacenado o reflejado» es peligroso. Aunque el servidor devuelva el valor como texto inofensivo o como JSON, si después JavaScript del lado del navegador lo asigna a innerHTML, se convierte en DOM Based XSS (el llamado stored DOM XSS, en el que un valor guardado es el origen). En ese caso, lo que hay que corregir no es el proceso de salida del servidor, sino el sumidero del lado del cliente, así que juzgue por dónde se volvió ejecutable, no por si la cadena es visible en la respuesta.
Qué hace el script incrustado (tocar el DOM, comunicarse, leer la cookie) y cómo entró ese script en la página son dos cuestiones que hay que pensar por separado. La confusión entre ambas no es solo un problema a la hora de elegir la contramedida: hace que se elija mal la contramedida. Si se juzga que es DOM Based XSS, la conclusión sería «basta con corregir el JavaScript del cliente», pero lo que realmente había que corregir era el proceso de salida del servidor.
Pregunta 1(2): la contramedida
La pregunta 1(2) pide, en 30 caracteres o menos, la contramedida para la aplicación web Q. El modelo de respuesta es «Aplicar un proceso de escapado antes de emitir el título de la reseña».
Aquí es importante el orden que se indica: «antes de emitir». No se trata de escapar cuando se recibe la entrada, sino justo antes de emitirla como HTML, según el contexto del destino de salida. La propia guía «Cómo crear un sitio web seguro» de IPA sitúa, como primera solución fundamental contra el XSS, «aplicar un proceso de escapado a todos los elementos que se emiten en la página web»5.
«¿No importa escapar en la entrada, como en la inyección SQL?»
Es la pregunta que siempre surge aquí. La respuesta corta es: tampoco en la inyección SQL «escapar en la entrada» es la contramedida.
Lo que IPA señala como solución fundamental contra la inyección SQL es «implementar toda la construcción de sentencias SQL mediante marcadores de posición (placeholders)». Se menciona el escapado como alternativa para cuando no queda más remedio que construir la sentencia SQL por concatenación de cadenas, pero incluso ahí se indica que «cuando la construcción de la sentencia SQL se realice mediante concatenación de cadenas, se deben formar correctamente los literales de la sentencia SQL usando las API del motor de base de datos que realizan el escapado, entre otras funciones»; es decir, el escapado se hace en el momento de construir la sentencia SQL6, no cuando se recibe la entrada.
Es exactamente la misma estructura que en el caso de HTML. El principio común queda así:
El escapado se hace en el lugar donde se decide a qué gramática va a salir el dato.
- Si va a salir como HTML, escapado de HTML
- Si va a formar parte de una sentencia SQL, marcadores de posición (o, si no queda más remedio, escapado como literal SQL)
- Si va a formar parte de un comando de shell, el tratamiento propio del shell
¿Por qué no se debe hacer en la entrada? Porque en el momento de recibir la entrada todavía no se sabe a dónde va a salir ese dato. El mismo cuerpo de reseña puede acabar en una página HTML, en una exportación CSV, en una respuesta JSON de una API, en un correo de notificación y en un registro. Si se aplica escapado HTML en la entrada, en el CSV aparecerá literalmente la cadena &, y el contenido de la base de datos quedará distinto del valor original, con lo que la búsqueda y los agregados se descuadran. Además, es caldo de cultivo para el doble escapado.
Entonces, ¿la entrada no debe hacer nada?
No es así. Lo que corresponde hacer en la entrada no es escapado, sino validación. Son funciones distintas.
- «Rechazar» en la entrada — no aceptar valores que la especificación no admite. En un campo de código postal, rechazar todo lo que no sean 7 dígitos numéricos; en un campo de cantidad, rechazar números negativos. Es un proceso independiente, necesario para preservar la corrección de los datos
- «Escapar» en la salida — representar de forma segura, en la gramática del destino, los datos ya aceptados. Esta es la solución fundamental de la vulnerabilidad
Y es importante no esperar demasiado de la validación de entrada como contramedida de XSS. Sobre el XSS, IPA menciona el método de comprobar que el valor de entrada se ajusta a la especificación de la aplicación, pero aclara expresamente que la eficacia de esta contramedida es limitada, y que, cuando la especificación de la aplicación permite entradas con una gran variedad de caracteres, no funciona como contramedida, por lo que no se recomienda confiar en este método5.
En este caso, el título y el detalle de la reseña eran precisamente campos de texto libre que admitían una gran variedad de caracteres. Hay un límite de 50 o 300 caracteres, pero el número de caracteres necesario para ejecutar un script es mucho menor, así que la mera existencia de un límite no supone ninguna defensa. Qué ocurrió exactamente en la empresa Q lo veremos en la pregunta 2.
5. Cómo se sorteó el límite de 50 caracteres — pregunta 2
La pregunta 2 es la joya de este problema.
Sobre la figura 3, responda en 50 caracteres o menos el método por el que se logró que se ejecutara un script de mayor longitud que el límite de caracteres de entrada.
El modelo de respuesta es «Se realizaron varias publicaciones divididas de modo que el HTML quedara comentado y todo se convirtiera en un único script».
El atacante dividió el contenido en fragmentos que cabían en una sola publicación y publicó 15 veces. El punto clave es cómo se trata el HTML que necesariamente se intercala entre publicación y publicación (</div>, <div class="...">, etc.), que en JavaScript sería un error de sintaxis.
Para eso se usaron los comentarios de bloque de JavaScript. A continuación se muestra una versión simplificada, escrita por nuestra empresa para explicar el mecanismo (no es una cita literal de la figura del cuadernillo).
Supongamos que se publica lo siguiente en el campo de título de reseña, dividido en 3 partes.
Publicación 1: Es estupenda<script>a=1;/*
Publicación 2: */b=2;/*
Publicación 3: */c=3;</script>
El servidor emite esto en el HTML como título de cada reseña, así:
<div class="review-title">Es estupenda<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>
Desde el punto de vista del navegador, desde el primer <script> hasta el último </script> hay un único elemento script. Su contenido queda así:
a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;
El HTML que queda entre /* y */ se salta como comentario de JavaScript, y lo que realmente se ejecuta es solo a=1; b=2; c=3;. Y la parte que se salta como comentario, al seguir siendo contenido de un elemento script, no se muestra en pantalla. Este es el origen real del síntoma de «16 reseñas que se ven como 2».
Qué llevarse de aquí
No confunda la causalidad. El límite de caracteres de entrada no falló como contramedida de XSS porque el número de publicaciones fuera ilimitado. Falló porque con 50 caracteres ya basta para ejecutar un script. Basta con añadir un solo atributo de manejador de eventos, o colocar una sola etiqueta que cargue un script externo, y cualquiera de las dos cabe en unas pocas decenas de caracteres. Aunque se limitara la publicación a una sola, el límite de caracteres no habría sido una defensa.
Entonces, ¿qué fue la publicación dividida de este caso? Fue un medio para introducir de una sola vez un script largo de unas 20 líneas. El atacante ganó extensión a base de repeticiones porque lo que quería hacer era largo; eso no es, en sí mismo, la razón por la que se sorteó el límite de caracteres. Hay que separar la descripción del procedimiento de la evaluación de la contramedida.
Lo mismo vale para las demás «restricciones del lado de la entrada» en general.
- El límite de caracteres, la restricción de tipos de caracteres admitidos, la validación en el frontend: son necesarias por especificación, pero no sustituyen a la solución fundamental del XSS
- El atacante siempre tiene margen para sortear las restricciones de entrada dividiendo el contenido, codificándolo o introduciéndolo por otra vía
- El lugar que hay que proteger es el momento en que el dato sale como HTML
El comentario de calificación de IPA también dice, sobre esta pregunta, que «se observaron algunas respuestas, aparentemente con verificación insuficiente, del tipo “publicó después de eliminar el límite de entrada con las herramientas de desarrollador”». Es cierto que la restricción del frontend se puede quitar con las herramientas de desarrollador. Pero lo que pedía esta pregunta era leer, a partir de los rastros que quedaron en el HTML, qué se hizo realmente. En el HTML de la figura 3, la publicación aparecía dividida en 15 fragmentos que caben dentro del límite, cada uno con marcas de comentario al principio y al final. Eso no es el rastro de haber «quitado» el límite, sino de haberlo «sorteado». Debe leerse como una pregunta que exigía confirmar, uno por uno, lo que había dejado el atacante.
El enfoque de diseño sobre hasta dónde y cómo validar los valores de entrada también se trata, como diseño que no confía en los datos que entran desde fuera, en Cómo validar en la aplicación el valor leído de un código QR.
6. Qué hacía el script — pregunta 3(1)
El script que extrajo N tiene unas 20 líneas. La pregunta 3(1) pide, en 60 caracteres o menos, el contenido de lo que procesan las líneas 6 a 20 (la parte que se ejecuta después de que la primera comunicación tenga éxito).
El modelo de respuesta es «Carga el ID de sesión como imagen de icono, junto con el token obtenido de la respuesta del XHR».
A continuación se muestra código equivalente escrito por nuestra empresa para explicar este comportamiento (no es una cita literal de la figura del cuadernillo).
// ① Primero obtiene la página de configuración de perfil
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document"; // Recibe la respuesta como DOM en lugar de texto
xhr.send();
xhr.onload = function () {
// ② Lee el token de carga siguiendo el mismo procedimiento que la pantalla legítima
const token = xhr.response.getElementById("token").value;
// ③ Convierte la cookie (que incluye el ID de sesión) directamente en el contenido de un archivo PNG
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ Lo envía a la función de carga de imagen de icono del propio sitio
const form = new FormData();
form.append("uploadfile", file);
form.append("token", token);
const xhr2 = new XMLHttpRequest();
xhr2.open("post", "https://example.jp/user/upload");
xhr2.send(form);
};
Las primeras 5 líneas existen solo para pasar la contramedida CSRF de la empresa Q
Lo que llama la atención de este script es que la primera mitad se dedica por completo a obtener el token.
La empresa Q exigía, para cargar la imagen de icono, «que coincida con el token emitido en /user/profile». Este es un mecanismo para evitar que un formulario falso colocado en otro sitio dispare una carga sin consentimiento, es decir, una contramedida de CSRF.
Pero el script de ataque se ejecuta dentro del navegador de la víctima, en el mismo origen que la víctima. Entonces basta con hacer exactamente lo mismo que hace la pantalla legítima: hacer un GET a la página de configuración de perfil y leer el valor del elemento token que contiene. Con eso ya se obtiene el token.
El token de protección CSRF no impide el XSS. En el momento en que el XSS se consuma, el código del atacante puede comportarse como «el usuario legítimo con sesión iniciada». Lo que protege el token es «una solicitud enviada sin consentimiento desde otro origen», no «un script malicioso que se ejecuta en el mismo origen».
Esta distinción es útil también al leer requisitos de seguridad en el trabajo diario. La afirmación «tenemos un token CSRF, así que estamos protegidos» puede ser correcta para el CSRF, pero no dice nada sobre el XSS.
Qué significa el objeto File
La línea ③ también merece atención. Crea un objeto de archivo cuyo contenido es la cadena de document.cookie, con nombre de archivo a.png y tipo MIME image/png.
El contenido es solo texto. No es un archivo PNG. Que la carga tuviera éxito de todos modos se debe a que, como indica el enunciado, la aplicación web Q no verificaba el formato de los archivos de imagen cargados.
Y que document.cookie pudiera leerse se debe a que la cookie no tenía el atributo HttpOnly. La RFC 6265 define, sobre el atributo HttpOnly, que «limita el alcance de la cookie a las solicitudes HTTP. En particular, instruye al agente de usuario a omitir la cookie cuando proporciona acceso a las cookies mediante API “no HTTP”, como aquellas de un navegador web que exponen las cookies a los scripts»7. Si ese atributo hubiera estado presente, la cadena que captura la línea ③ habría carecido del ID de sesión, y lo extraído habría sido inútil para el atacante.
Aquí conviene tener presente que lo que oculta HttpOnly es solo la cookie que tiene ese atributo. Si en el mismo origen hay otras cookies sin HttpOnly (por ejemplo, para preferencias de visualización o para métricas), document.cookie seguirá devolviéndolas. No es que «poner HttpOnly vacíe document.cookie». Lo que hay que proteger es el ID de sesión, así que compruebe específicamente si la cookie del ID de sesión lo tiene.
7. Una exfiltración de información que no sale al exterior — pregunta 3(2)(3)
La pregunta 3(2) pregunta, en 50 caracteres o menos, «cómo puede el atacante obtener la información cargada». El modelo de respuesta es «Descargar la imagen de icono del socio y extraer de ahí la cadena del ID de sesión».
Recuerde lo que decía la especificación del enunciado:
La imagen de icono cargada se muestra en la página de configuración del perfil del socio y en las páginas de reseñas.
Es decir, la «imagen de icono» donde quedó escrito el ID de sesión de la víctima se coloca en un lugar visible dentro del propio sitio. El atacante no necesita hacer nada especial. Solo tiene que abrir una página donde se muestre la imagen de icono de la víctima y obtener la URL de esa imagen. Como el contenido es texto, al abrirlo puede leer el ID de sesión.
Como aclaración, esta recuperación es directa siempre que la imagen de icono de la víctima aparezca en alguna página visible para el atacante (por ejemplo, la página de reseñas de un producto en el que el propio atacante también haya publicado una reseña). Como la URL del icono está fijada por socio, en cuanto se ve una vez en algún sitio, después se puede ir directamente a esa URL. En cualquier caso, la operación del atacante consiste solo en hacer un GET a páginas e imágenes como cualquier visitante legítimo, y desde el punto de vista del sitio no es una comunicación anómala.
sequenceDiagram
autonumber
participant AT as Atacante
participant Q as Aplicación web Q<br/>(example.jp)
participant V as Navegador de la víctima
Note over AT,Q: Fase de preparación
AT->>Q: Publica la reseña dividida en 15 partes<br/>(salta el HTML con marcas de comentario)
Note over Q: Guarda la cadena publicada<br/>tal cual<br/>(este guardado es correcto)
Note over V,Q: La víctima abre la página V
V->>Q: GET página V
Note over Q: ★Aquí está la vulnerabilidad★<br/>Incrusta en el HTML el título<br/>guardado sin escapar
Q-->>V: HTML que incluye el script de ataque
Note over V: Se ejecuta como elemento script
V->>Q: GET /user/profile
Q-->>V: HTML que incluye el token
Note over V: Lee document.cookie y lo<br/>convierte en el contenido de a.png
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: Guarda sin verificar el formato<br/>y lo publica como imagen de icono
Note over AT,Q: Recuperación
AT->>Q: GET imagen de icono de la víctima
Q-->>AT: Archivo con el ID de sesión escrito
Figura 1: cadena completa del ataque, desde la publicación dividida hasta la recuperación del ID de sesión.
Las contramedidas de salida no lo detienen
Lo problemático de esta ruta se aprecia con claridad en el diagrama. El navegador de la víctima, del principio al fin, solo se comunica con el sitio legítimo (example.jp).
Como resultado, las contramedidas que vigilan la comunicación fallan una tras otra. Solo funciona la última.
| Contramedida | Frente a este ataque | Motivo |
|---|---|---|
| Filtrado de URL del proxy | No funciona | El destino accedido es únicamente el sitio de comercio electrónico legítimo del negocio |
| Vigilancia de tráfico saliente del cortafuegos | No funciona | No se produce comunicación hacia ningún dominio desconocido |
Restricción connect-src de la Content Security Policy (por ejemplo, permitiendo el mismo origen con 'self') |
No funciona | Las 2 comunicaciones tienen ambas como destino el mismo origen, y quedan dentro del alcance que permite la política |
Restricción script-src de la Content Security Policy (configuración que no permite 'unsafe-inline') |
Sí funciona | El script en línea incrustado directamente no llega a ejecutarse |
Al pensar en la exfiltración de información mediante XSS, es fácil imaginar la escena de «la cookie se envía al servidor del atacante», pero este problema deja claro que el destino de la exfiltración puede ser el propio sitio de la víctima. Si dentro del sitio existe un lugar que se pueda escribir y también leer, ese lugar se convierte en el punto de entrega. Las funciones de carga de archivos, los campos de perfil público o los campos de notas públicas son ejemplos típicos.
Sobre la CSP, entre las directivas de uso habitual en la práctica, la que puede frenar con certeza este ataque no es connect-src (restricción del destino de comunicación), sino script-src. Con script-src 'self' y sin permitir 'unsafe-inline', el <script> en línea incrustado en la reseña no llegaría a ejecutarse. Entender la CSP solo como «algo que restringe la comunicación hacia el exterior» hace pasar por alto esta diferencia.
Tampoco connect-src es inútil en principio. Una restricción como connect-src 'none', que prohíbe incluso la comunicación hacia el mismo origen, o una lista de permitidos que limite el destino a puntos finales concretos, también detendría estas 2 llamadas XHR. Pero como son pocos los sitios que pueden llegar a ese nivel de restricción, en la práctica la medida realista es script-src. La comprensión correcta no es que «el mismo origen queda fuera del alcance de la CSP», sino que «con las configuraciones habituales que permiten el mismo origen, esta comunicación pasa igualmente».
Pregunta 3(3): qué se puede hacer con el ID de sesión
La pregunta 3(3) pregunta, en 40 caracteres o menos, qué se puede hacer con la información obtenida. El modelo de respuesta es «Suplantar al socio que accedió a la página V y usar las funciones de la aplicación web Q».
Según el comentario de calificación, la tasa de aciertos de esta pregunta fue alta. Se indica que se comprendía bien el impacto de que un atacante obtenga la cookie en un sitio de comercio electrónico.
Y si se relee la especificación del enunciado, la función de perfil del socio incluía una página para registrar los datos de la tarjeta de crédito, disponible solo para socios con sesión iniciada. El enunciado ya había dejado preparado, desde el principio, a dónde lleva la suplantación.
8. Por qué no funciona desde el sitio del atacante — pregunta 4
La pregunta 4 es, dentro de este problema, la más esencial.
Suponga que el atacante prepara, en un sitio de un dominio de su elección, un HTML con el mismo script de la figura 4, y que un socio con sesión iniciada en la aplicación web Q accede a ese sitio. Aun así, el ataque no tendría éxito debido a un mecanismo del navegador web. Responda ese mecanismo en 40 caracteres o menos.
El modelo de respuesta es «El mecanismo por el que la cookie no se envía desde el script a URL de un dominio distinto».
Supongamos que se coloca el mismo script en evil.example y se hace que un socio de la empresa Q lo abra. ¿Qué ocurre?
Primero, la primera comunicación falla. Cuando el script en evil.example envía un XMLHttpRequest a https://example.jp/user/profile, esto es una solicitud de origen cruzado.
Lo decisivo es que no se adjunta la cookie. El script de la figura 4 no especifica withCredentials, así que en una solicitud de origen cruzado no se adjunta la cookie. Desde el punto de vista del servidor de la empresa Q, es un acceso sin sesión iniciada, y no se devuelve la página de configuración de perfil que contiene el token. El script no puede obtener el token, y ahí se detiene.
Hay otra barrera más: no se puede leer la respuesta. Si la empresa Q no permite evil.example en Access-Control-Allow-Origin, el navegador falla la verificación de CORS y trata esta comunicación como un error de red; onload no se dispara y no se puede llegar a xhr.response. Como un sitio normal no devuelve esta cabecera para un origen desconocido, en la práctica el ataque también se detendría aquí.
Ahora bien, el enunciado no dice cómo tenía configurado el CORS la empresa Q. Aunque hubiera permitido el acceso desde evil.example, lo que se podría leer sería una respuesta en estado sin sesión iniciada, que no contiene el token, así que el ataque tampoco tendría éxito. Con solo el primer punto, que la cookie no se adjunta, el ataque ya queda detenido.
Aunque cualquiera de los dos bastaría por sí solo para detener el script, en la práctica ambos actúan a la vez.
Además, tampoco se puede leer la cookie. Lo que devuelve document.cookie es la cookie del origen en el que se ejecuta ese script (evil.example). El ID de sesión emitido para example.jp no está ahí.
El modelo de respuesta de IPA se refiere al primero de estos dos hechos, es decir, «la cookie no se envía a la URL de un dominio distinto».
No memorice estos dos como si fueran el mismo mecanismo. Son mecanismos distintos.
- Que
document.cookieno devuelva la cookie deexample.jpse debe a que las cookies se gestionan aisladas por dominio. Entre sitios distintos (dominios registrables distintos), comoevil.exampleyexample.jp, este aislamiento no se puede relajar mediante configuración. Ahora bien, entre subdominios del mismo dominio es distinto: usando el atributoDomainde la cookie es posible compartirla entresub.example.jpyexample.jp7 - Que no se adjunte la cookie en un XHR de origen cruzado se debe a que
withCredentialsesfalsepor defecto. Esto sí puede cambiar si se dan las condiciones. Si el script especificawithCredentials = true, el atributoSameSitede la cookie permite el envío entre sitios, y el servidor devuelveAccess-Control-Allow-Origincon el origen de procedencia explícito junto conAccess-Control-Allow-Credentials: true, la cookie sí se envía y la respuesta sí se puede leer
Aquí conviene separar la razón por la que realmente falló de lo que haría falta si se reescribiera el ataque.
Lo único que se puede afirmar con certeza a partir del enunciado y la figura 4 es esto: como no se especifica withCredentials, la cookie no se adjunta, el acceso se trata como sin sesión iniciada y no se obtiene el token. Con esto solo, el ataque ya queda detenido.
Que no haya permiso de CORS también es una barrera que impide leer la respuesta, pero como la configuración de la empresa Q no se indica en el enunciado, considere esto como un refuerzo condicional del tipo «un sitio normal suele estar así».
¿Y si el atacante reescribiera el código con withCredentials = true? Aquí entra en juego otra condición. Si el SameSite de la cookie no permite el envío entre sitios, aunque se active withCredentials, esa cookie tampoco se adjuntaría. Los navegadores actuales tratan una cookie sin SameSite especificado como Lax, así que, por defecto, la balanza se inclina hacia no adjuntarla.
Además, el enunciado no dice cómo estaba configurado el SameSite de la cookie del ID de sesión de la empresa Q. Por lo tanto, no se puede afirmar que «estaba protegida gracias al SameSite». Se trata únicamente de que SameSite puede ser eficaz frente a un adversario que ataca con credenciales incluidas.
En resumen, para leer desde otro dominio una respuesta en estado autenticado hacen falta 3 condiciones a la vez: withCredentials = true, que el SameSite de la cookie permita el envío entre sitios, y que el servidor permita, mediante CORS con credenciales, el origen de procedencia. No es que un dominio distinto sea siempre seguro por definición, pero tampoco basta con que se cumpla una sola condición para romperlo. Al revisar el propio sitio, en lugar de confiar en el aislamiento por dominio de las cookies, hay que comprobar realmente la configuración de CORS (en particular, a qué orígenes se permiten solicitudes con credenciales) y el atributo SameSite de las cookies.
Por eso el XSS almacenado tiene tanto valor
Esta pregunta enseña la razón por la que el atacante insiste en «ejecutar su propio código dentro del sitio de la víctima».
Aunque se prepare, en el propio dominio, un sitio falso visualmente idéntico, el navegador lo trata como un sitio distinto. No se puede leer la cookie del sitio contrario desde document.cookie, y con un XHR enviado sin credenciales, como el de esta pregunta, ni siquiera se llega a tener el estado de sesión iniciada visto desde el sitio contrario. El valor para el atacante está precisamente en que el código se ejecute en el origen del sitio de la víctima. El XSS almacenado es justamente el medio que lo consigue.
Ahora bien, no interprete que «desde un dominio distinto nunca se puede enviar ninguna solicitud con credenciales». Si el SameSite de la cookie permite el envío entre sitios (por ejemplo, SameSite=None), desde una página maliciosa sí es posible hacer llegar al servidor de destino una solicitud con la cookie adjunta. El envío de un formulario por POST es el ejemplo típico; lo que controla CORS es, sobre todo, si el script puede leer la respuesta, no si la solicitud llega al servidor (aunque, si se añaden cabeceras personalizadas o se envía como application/json, entre otros casos, se antepone una solicitud de verificación previa (preflight), y si esta no se permite, la solicitud principal ni siquiera se envía). Esta es la razón por la que existe el ataque CSRF, y precisamente por eso hace falta el token de protección CSRF.
Es decir, lo que un script de otro origen no puede hacer por defecto es «leer la cookie del sitio contrario» y «leer la respuesta»; no es «enviar la solicitud».
Y estos dos «no se puede leer» tienen fuerzas distintas. El aislamiento por dominio de document.cookie no se puede relajar con la configuración del servidor, pero si se puede leer la respuesta depende de la configuración de CORS del servidor de destino. Si el servidor permite el origen de procedencia (con credenciales, devolviendo un origen concreto en Access-Control-Allow-Origin junto con Access-Control-Allow-Credentials: true), un script de otro origen sí puede leer la respuesta. Aquí está la razón por la que conviene revisar la configuración de CORS del propio sitio.
Lo especial del XSS es que salta por encima de todas estas condiciones desde el principio y puede comportarse, ya de entrada, como el origen del sitio de la víctima.
Dicho de otro modo, el XSS no es «un fallo por el que aparecen caracteres raros en pantalla», sino un fallo que pone al atacante en la misma posición que un usuario legítimo del propio sitio. Esta diferencia de percepción es la que separa cómo se prioriza este riesgo.
9. Las contramedidas que funcionaron y las que no
Reunimos todo lo anterior en una sola tabla. Es la parte que más nos gustaría que se llevara de este artículo.
| Mecanismo que tenía o no tenía la empresa Q | Frente a este ataque | Motivo |
|---|---|---|
| Comunicación por HTTPS | No funcionó | Protege el canal de comunicación, sin relación con el script incrustado en la página |
| Límite de entrada de 50 caracteres en el título y 300 en el detalle de la reseña | No funcionó | Se publicó dividido en 15 veces, y el HTML intercalado se saltó como comentario de JavaScript |
| Token de carga (contramedida CSRF) | No funcionó | Un script del mismo origen puede obtener el token por el procedimiento legítimo |
| Función de carga que exige sesión iniciada | No funcionó | Quien la ejecuta es el propio navegador de la víctima, ya con sesión iniciada |
| Escapado en la salida (no lo tenía) | Funciona (solución fundamental) | La cadena publicada deja de interpretarse como HTML, así que el script directamente no se ejecuta |
| Atributo HttpOnly de la cookie (no lo tenía) | Funciona (contramedida complementaria) | El ID de sesión deja de poder leerse desde document.cookie |
| Verificación del formato del archivo cargado y reprocesado (no lo tenía) | Funciona (contramedida complementaria) | El texto plano no puede guardarse como PNG, y la información oculta en los metadatos también desaparece con el reprocesado. Sin embargo, si se codifica como píxeles de la imagen, persiste |
Las 4 primeras filas no cumplieron ninguna función frente a este ataque. Situando las 3 últimas en la clasificación de «Cómo crear un sitio web seguro» de IPA, la primera es solución fundamental y las otras 2 son contramedidas complementarias5.
Una aclaración: las 4 primeras no significan «algo que IPA no reconoce como contramedida». La clasificación de IPA tiene dos categorías, solución fundamental y contramedida complementaria, y la verificación del contenido del valor de entrada se menciona precisamente como contramedida complementaria. Pero lleva la advertencia de que «la eficacia de esta contramedida es limitada», y en este caso se dio exactamente ese límite.
Y lo importante es que con cualquiera de las 3 últimas habría bastado para romper la cadena del ataque ejecutado en este problema.
- Si hubiera escapado, el script directamente no se habría ejecutado
- Si hubiera tenido HttpOnly, el script se habría ejecutado pero no habría podido leer la cookie
- Si hubiera verificado el formato, se habría podido leer la cookie, pero no se habría podido cargar el texto plano como PNG
La defensa en profundidad tiene sentido precisamente en escenas como esta. Ahora bien, las contramedidas complementarias no sustituyen a la solución fundamental. Aunque tenga HttpOnly, si persiste el XSS, el atacante puede ejecutar en el navegador de la víctima cualquier operación (comprar un producto, cambiar los datos registrados, etc.). Se pueden diseñar ataques que ni siquiera necesitan robar el ID de sesión.
Por qué se escribió «verificación de formato» y «reprocesado» en dos pasos
Hay una razón para haber escrito la última fila de la tabla como «verificación de formato y reprocesado» en dos etapas.
La empresa Q del enunciado no verificaba en absoluto el formato de los archivos de imagen, así que con solo añadir la verificación de formato el ataque se detendría. Pero eso solo vale para el caso en que el atacante envía texto plano. El atacante también puede construir un archivo PNG legítimo y ocultar la cadena en su zona de metadatos, por ejemplo. En ese caso, la comprobación de «si el formato es correcto» pasaría igualmente.
Por eso, para reducir realmente esta ruta, hace falta llegar hasta reprocesar en el servidor la imagen cargada antes de distribuirla (no guardar tal cual la secuencia de bytes original).
Ahora bien, ni siquiera el reprocesado cierra la ruta por completo. Si el atacante escribe el ID de sesión directamente como píxeles de la imagen y construye un PNG legítimo, un reprocesado normal conserva esa información visual, así que el atacante podría descargarla y leerla igualmente. Lo que detiene el reprocesado son la variante que envía la secuencia de bytes en bruto y la variante que la oculta en la zona de metadatos, no la información codificada en el propio contenido de la imagen.
Es decir, mientras exista dentro del sitio un lugar «donde se pueda escribir y también leer», no es posible cerrar por completo esa ruta de exfiltración. Esto ilustra bien la naturaleza de las contramedidas complementarias. No basta con que una contramedida complementaria detenga «el ataque observado hasta ahora»: hay que diseñarla teniendo en cuenta también si sigue funcionando cuando el atacante cambia ligeramente de técnica. Y por muchas capas que se acumulen, nunca sustituyen a la solución fundamental del escapado en la salida.
Además, un diseño habitual que suele recomendarse es distribuir los archivos cargados desde un dominio distinto al principal, pero esto no es una contramedida frente a esta ruta de exfiltración. El atacante puede descargar la imagen igualmente desde un dominio distinto, y la política de mismo origen no es un mecanismo que mantenga en secreto una secuencia de bytes que ya es pública. Distribuir desde otro dominio sí es eficaz frente a la amenaza de que el propio archivo cargado se ejecute como contenido activo en el origen del sitio de la víctima (el caso de que se pueda cargar HTML o SVG, por ejemplo). Es una contramedida para una amenaza distinta, y conviene entenderlas por separado.
La idea de juzgar un archivo por su contenido, en lugar de por lo que declara el emisor (extensión o Content-Type), es un comportamiento básico necesario más allá de la Web. Ahora bien, «juzgar por el contenido» significa algo distinto según el formato. En un formato como PNG, con una secuencia de bytes fija al principio, se puede verificar esa secuencia. En cambio, el CSV no tiene una firma estandarizada, y el BOM al principio solo indica la codificación de caracteres, no es prueba de que el contenido sea CSV. Como se trató en Manejo práctico de archivos CSV, en el caso del CSV lo que corresponde comprobar es si el análisis (parsing) se completa con el dialecto que se espera admitir, y si se ajusta al esquema y al límite de filas previstos.
10. Puntos a revisar en el propio código
Si se traslada esta pregunta a una lista de verificación de revisión de código, queda así. Sirve directamente para cualquier aplicación web con funciones de socios o de publicación.
- ¿El escapado en la salida funciona en todos los puntos de salida? Localice todos los sitios donde se ha desactivado explícitamente el escapado automático del motor de plantillas (
raw,| safe,dangerouslySetInnerHTML, etc.) y compruebe si se puede justificar, en cada uno, por qué era correcto desactivarlo - ¿Se está contando alguna restricción del lado de la entrada como contramedida de XSS? El límite de caracteres o la restricción de tipos de caracteres admitidos son requisitos de especificación, no la solución fundamental del XSS
- ¿No se está escapando en la entrada? La función de la entrada es la validación, que rechaza valores fuera de especificación, no el escapado. En el momento de la entrada aún no está decidido el destino de salida (HTML, CSV, JSON, correo, registro), así que escapar en la entrada provoca doble escapado y corrupción de datos. Lo mismo vale para la construcción de sentencias SQL: la solución fundamental son los marcadores de posición
- ¿La cookie tiene
HttpOnly,SecureySameSite? Compruebe especialmente la cookie del ID de sesión - ¿Se verifica el formato del archivo cargado por su contenido, y no por la extensión o el Content-Type? La información que declara el emisor no sirve como material de verificación. Sin embargo, la sola verificación de formato deja pasar la técnica de ocultar datos en la zona de metadatos de una imagen legítima
- ¿Se reprocesa la imagen cargada antes de distribuirla? ¿Se está devolviendo tal cual la secuencia de bytes original? Tenga en cuenta que, incluso con reprocesado, la información codificada como píxeles persiste, así que un lugar «donde se puede escribir y también leer» no queda completamente cerrado. La distribución desde otro dominio, por su parte, es una contramedida frente a la amenaza de que el archivo cargado se ejecute en el origen del propio sitio, no frente a esta ruta de exfiltración
- ¿La CSP está configurada de modo que pueda detener los scripts en línea? El criterio no es «si aparece
'unsafe-inline'», sino «si realmente se permite un script en línea». Siscript-srcincluye un nonce o un hash, los navegadores compatibles con CSP nivel 2 o posterior ignoran'unsafe-inline'; así, en una política de compatibilidad hacia atrás que combina'unsafe-inline'con nonce, un script inyectado sin nonce queda bloqueado - ¿Puede explicar qué protege cada token? El token de protección CSRF no impide el XSS. Cada uno necesita su propia contramedida
El punto 1 en particular es el patrón típico que aparece en las investigaciones reales. No es raro encontrar casos en los que se confiaba en que el framework escapaba automáticamente, pero por un requisito puntual («queremos insertar este HTML tal cual») se había desactivado el escapado automático en un solo lugar.
Para terminar — la capacidad que este problema realmente evalúa
El repaso de contramedidas que se ha visto hasta aquí está resumido al principio, en «Primero, la conclusión». Por último, una nota sobre el propio problema.
Lo que hace bueno a este examen no es que pregunte «¿conoce el XSS?», sino que evalúa si se sabe distinguir, entre las contramedidas ya disponibles, cuáles funcionan realmente.
La empresa Q no es que no hiciera nada. Se comunicaba por HTTPS, ponía límites de caracteres en la entrada, exigía un token para la carga y restringía las funciones a socios con sesión iniciada. Puesto todo en fila, parece un diseño razonablemente protegido. Y aun así, todo se sorteó. En cambio, los 3 elementos que faltaban son discretos, del tipo que nunca aparecería en una nota de lanzamiento.
Lo que hace difícil la discusión sobre seguridad es que el número de contramedidas y la solidez de la defensa no son proporcionales. Poder decir, una por una, no «qué se está haciendo», sino «esa contramedida, frente a qué ataque y en qué etapa, lo detiene»: esta no es una capacidad exclusiva del examen, sino la más necesaria tanto para quien recibe explicaciones de seguridad como para quien las implementa.
Áreas de consultoría relacionadas
KomuraSoft LLC se dedica al desarrollo de sitios web con funciones de socios o de publicación de contenido, y a la revisión de diseño de aplicaciones web ya existentes para identificar si tienen el mismo tipo de agujero.
- Desarrollo y renovación de sitios web
- Consultoría técnica y revisión de diseño
- Investigación de fallos y análisis de causas
- Contacto
Referencias
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Cuadernillos de examen, ponderación de puntos, modelos de respuesta y comentarios de calificación (año fiscal 2023, Reiwa 5), correspondiente a «Examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5, tarde, cuadernillo de preguntas». Sobre las funciones de la aplicación web Q de la empresa Q (registro de socios, inicio de sesión y emisión del ID de sesión mediante cookie, límites de entrada de 50 caracteres en el título y 300 en el detalle de la función de reseñas de producto, la función de perfil del socio con carga de imagen de icono y registro de datos de tarjeta de crédito, que la carga de la imagen de icono use un archivo de imagen y un token como parámetros, y que la imagen de icono cargada se muestre en la página de configuración del perfil del socio y en las páginas de reseñas), el fenómeno de que en la página V se muestren solo 2 de las 16 reseñas esperadas, el hecho de que el HTML de la página V contuviera 15 publicaciones del socio A junto con un script largo, el contenido del script extraído, y la existencia de una vulnerabilidad en la aplicación web Q por la que se ejecutaba un script introducido por un socio, la falta del atributo HttpOnly en la cookie y la ausencia de verificación del formato de los archivos de imagen cargados. El texto de las preguntas 1 a 4 también procede de este cuadernillo. ↩ ↩2
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Preguntas frecuentes sobre los exámenes. Sobre el hecho de que, para el uso de los exámenes anteriores que publica esta agencia, no se necesita autorización ni pago alguno salvo que una ley lo establezca expresamente; que, no obstante, la agencia no renuncia a los derechos de autor; que es necesario indicar la fuente en el formato «año fiscal, convocatoria, categoría de examen, franja horaria, número de pregunta, etc.» (se muestra como ejemplo «Fuente: Examen de Fundamentos de Tecnología de la Información, primavera de Heisei 31, mañana, pregunta 1»); y que, cuando se haya modificado parte de una pregunta, también es necesario indicarlo. ↩
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5, modelos de respuesta. Sobre el objetivo de la pregunta 1 (evaluar, a partir de la respuesta a un incidente causado por la explotación de una vulnerabilidad de un programa de aplicación web, la capacidad de interpretar, a partir de HTML y ECMAScript, la vulnerabilidad explotada y el problema, y de proponer contramedidas) y los modelos de respuesta de cada pregunta (pregunta 1(1): «イ XSS almacenado»; pregunta 1(2): «Aplicar un proceso de escapado antes de emitir el título de la reseña.»; pregunta 2: «Se realizaron varias publicaciones divididas de modo que el HTML quedara comentado y todo se convirtiera en un único script.»; pregunta 3(1): «Carga el ID de sesión como imagen de icono, junto con el token obtenido de la respuesta del XHR.»; pregunta 3(2): «Descargar la imagen de icono del socio y extraer de ahí la cadena del ID de sesión.»; pregunta 3(3): «Suplantar al socio que accedió a la página V y usar las funciones de la aplicación web Q.»; pregunta 4: «El mecanismo por el que la cookie no se envía desde el script a URL de un dominio distinto»). ↩ ↩2
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Examen de Especialista en Seguridad de la Información Registrado, otoño de Reiwa 5, comentario de calificación. Sobre la tasa de aciertos media de la pregunta 1 en conjunto; sobre la pregunta 1(1), que «se observaron algunas respuestas erróneas de “DOM Based XSS”, posiblemente porque el script usaba el DOM»; la indicación de que conviene comprender las vulnerabilidades con precisión, incluyendo sus características y sus contramedidas; sobre la pregunta 2, que «se observaron algunas respuestas, aparentemente con verificación insuficiente, del tipo “publicó después de eliminar el límite de entrada con las herramientas de desarrollador”»; la indicación de que conviene cultivar la capacidad de examinar con atención los rastros que deja el atacante y comprender con precisión el método de ataque; y sobre la pregunta 3(3), que tuvo una alta tasa de aciertos y que se comprendía bien el impacto de que un atacante obtenga la cookie en un sitio de comercio electrónico. ↩ ↩2
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Cómo crear un sitio web seguro. Sobre la presentación, para los 11 tipos de vulnerabilidad de sitios web, de las amenazas y contramedidas divididas en «solución fundamental» (implementación que elimina la causa misma de la vulnerabilidad) y «contramedida complementaria» (contramedida que reduce la probabilidad de éxito del ataque o el daño cuando la vulnerabilidad persiste); que se señala, como solución fundamental contra el cross-site scripting, aplicar un proceso de escapado a todos los elementos que se emiten en la página web; y sobre la lista de verificación de implementación de seguridad adjunta y los anexos «Cómo invocar SQL de forma segura» y «Especificación del chequeo de salud web». ↩ ↩2 ↩3
-
IPA (Agencia de Promoción de Tecnología de la Información de Japón), Cómo crear un sitio web seguro - 1.1 Inyección SQL. Sobre el hecho de que, como solución fundamental contra la inyección SQL, se señala «implementar toda la construcción de sentencias SQL mediante marcadores de posición»; que los marcadores de posición estáticos (sentencias preparadas) se consideran, en principio, incapaces de generar la vulnerabilidad; que, como implementación para cuando la sentencia SQL se construye por concatenación de cadenas, se indica que «cuando la construcción de la sentencia SQL se realice mediante concatenación de cadenas, se deben formar correctamente los literales de la sentencia SQL usando las API del motor de base de datos que realizan el escapado, entre otras funciones», es decir, que el escapado se aplica a la formación de los literales que componen la sentencia SQL (no en el momento de recibir la entrada); y que, además, se señalan como solución fundamental «no especificar directamente sentencias SQL en los parámetros que se pasan a la aplicación web», y como contramedidas complementarias «no mostrar directamente los mensajes de error en el navegador» y «otorgar a la cuenta de base de datos los privilegios adecuados». ↩
-
IETF, RFC 6265: HTTP State Management Mechanism, sección 4.1.2.6, «The HttpOnly Attribute». Sobre el hecho de que el atributo HttpOnly limita el alcance de la cookie a las solicitudes HTTP, e instruye en particular al agente de usuario a omitir la cookie cuando proporciona acceso a las cookies mediante API «no HTTP», como aquellas de un navegador web que exponen las cookies a los scripts. Junto con el hecho de que el atributo Secure (sección 4.1.2.5) restringe el envío de la cookie a canales seguros únicamente. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Análisis del examen de Especialista en Seguridad de la Información Registrado, primavera de Reiwa 6, tarde, pregunta 1 — el alg=none de JWT, la autorización de API y las medidas provisionales de WAF
A partir de la pregunta 1 de la tarde del examen de Especialista en Seguridad de la Información Registrado, primavera de Reiwa 6, se expl...
Lo que también debe saber quien encarga un sitio web — cómo usar la guía «Cómo crear un sitio web seguro» de la IPA como checklist
¿Con qué criterio revisar la seguridad de su sitio web? Explicamos las 11 vulnerabilidades de la guía IPA «Cómo crear un sitio web seguro...
Especialista en Seguridad de la Información, otoño de Reiwa 5, pregunta 2 de la tarde — Archivos sustraídos por el Wi-Fi de invitados
A partir de la pregunta 2 de la tarde de R5 otoño, explica cómo se filtran archivos por el Wi-Fi de invitados pese a bloquear el USB, y r...
Las 10 principales amenazas de seguridad de la información 2026 — cómo leer el ranking y qué deben abordar realmente las pymes
En el «Top 10 de amenazas de seguridad de la información 2026» de IPA, el ransomware repite en el 1.º puesto por 11.º año consecutivo, la...
Medidas de seguridad para pymes, por dónde empezar — Cómo recorrer la versión 4.0 de la «Guía de medidas de seguridad de la información para pymes» del IPA
¿Por dónde empezar la seguridad en una pyme? La guía v4.0 del IPA explica los seis principios básicos, la autoevaluación de 5 minutos y S...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de sitios web
En sitios web con funciones de socios o de publicación de contenido, cuestiones como el escapado en la salida o la configuración de atributos de cookies inciden directamente en la calidad del desarrollo.
Consultoría técnica y revisión de diseño
Identificar dónde puede haber el mismo agujero en una aplicación web existente, desde una perspectiva de revisión de diseño, es precisamente el tipo de trabajo que cubre nuestra consultoría técnica.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué el XSS de este problema es "almacenado" si el script manipula el DOM? ¿No sería DOM Based XSS?
- El tipo de vulnerabilidad se decide por "dónde" la cadena del atacante se convierte en código ejecutable, es decir, por si el punto de salida vulnerable (el sumidero, o sink) está en el servidor o en el navegador. En este problema, el servidor incrustaba directamente en el HTML el título de reseña que había publicado el atacante y lo devolvía tal cual. El sumidero está en el procesamiento de salida del servidor, y esa cadena quedaba guardada y se repartía después a todas las personas que abrieran la página, por lo que es XSS almacenado. El DOM Based XSS es la categoría en la que JavaScript del lado del navegador hace ejecutable un valor, por ejemplo asignándolo a innerHTML. Que el script incrustado use o no APIs del DOM como XMLHttpRequest o getElementById no tiene relación con el tipo de vulnerabilidad. Además, aunque el servidor devuelva un valor guardado como texto inofensivo o como JSON, si después JavaScript del lado del navegador lo pasa a innerHTML, se convierte en DOM Based XSS. No juzgue por si la cadena es visible en la respuesta, sino por dónde se volvió ejecutable. El propio comentario de calificación de IPA señala que se observaron respuestas erróneas de "DOM Based XSS", posiblemente porque el script usaba el DOM.
- El título de la reseña tenía un límite de entrada de 50 caracteres. ¿Cómo pudo ejecutarse un script tan largo?
- El atacante dividió el contenido en fragmentos que cabían en una sola publicación y los publicó en varias veces. La primera publicación abre una etiqueta script a mitad del título y, al final del título, abre un comentario de bloque de JavaScript. La segunda publicación cierra ese comentario al principio, escribe la siguiente instrucción y vuelve a abrir un comentario al final, y este patrón se repite. Con esto, todo el HTML que queda entre publicaciones (etiquetas div, por ejemplo) cae dentro de un comentario de JavaScript y se ignora, de modo que solo los fragmentos divididos se enlazan para ejecutarse como un único script. El límite de caracteres solo restringía la longitud de cada publicación individual: nunca restringió el número de publicaciones.
- ¿A dónde se envió el ID de sesión robado?
- Nunca se envió a un servidor externo. El script convirtió el contenido de la cookie directamente en el contenido de un archivo, lo llamó "a.png", le puso el tipo MIME "image/png" y lo envió a la propia función de carga de imagen de icono del sitio de comercio electrónico que usaba la víctima. Como la imagen de icono cargada se muestra en páginas como la de reseñas, el atacante solo tenía que descargar esa imagen como cualquier usuario y extraer el ID de sesión de su contenido. El navegador de la víctima nunca se comunicó con nada más que el sitio legítimo, así que no se generó ningún tráfico externo sospechoso: una ruta que el filtrado de URL de un proxy o la vigilancia de tráfico saliente de un cortafuegos no pueden detectar.
- Se suponía que la carga requería un token. ¿Por qué el ataque tuvo éxito de todos modos?
- Porque ese token es un mecanismo para rechazar solicitudes no autorizadas provenientes de otro sitio (una contramedida de CSRF), no una protección pensada para un script que se ejecuta en el mismo sitio. El script de ataque primero accedió a la página de configuración de perfil mediante XMLHttpRequest, obtuvo el token del mismo modo que lo haría la pantalla legítima y, con ese token, ejecutó la carga. Como el XSS hace que el código del atacante se ejecute en el mismo origen que la víctima, puede hacer cualquier cosa que un usuario legítimo pueda hacer. Un token de protección CSRF no impide el XSS.
- He oído que en la inyección SQL importa escapar en la entrada. ¿Es el XSS el único caso en el que se escapa en la salida?
- No. Tampoco en la inyección SQL "escapar en la entrada" es la contramedida. Lo que IPA señala como solución fundamental es "implementar toda la construcción de sentencias SQL mediante marcadores de posición (placeholders)". Se menciona el escapado como alternativa para cuando la sentencia SQL se construye por concatenación de cadenas, pero incluso ahí es para "formar correctamente los literales de la sentencia SQL", es decir, en el momento en que se construye la sentencia, no cuando se recibe la entrada. El principio común es este: el escapado se hace en el lugar donde se decide a qué gramática va a salir el dato. Si va a salir como HTML, escapado de HTML; si va a formar parte de una sentencia SQL, marcadores de posición; si va a formar parte de un comando de shell, el tratamiento propio del shell. La razón por la que no se debe escapar en la entrada es que, en ese momento, todavía no se sabe a dónde saldrá el dato: el mismo texto de reseña puede acabar en una página HTML, en una exportación CSV, en una respuesta JSON de una API, en un correo de notificación o en un registro. Si se aplica escapado HTML en la entrada, el CSV mostrará literalmente entidades como &, y el contenido de la base de datos quedará distinto del valor original, con lo que la búsqueda y los agregados se descuadran. Esto no significa que la entrada no deba hacer nada: su función es la validación, no el escapado, es decir, rechazar valores que la especificación no admite.
- ¿Qué contramedida práctica hay que llevarse de este problema?
- La solución fundamental es aplicar escapado, justo antes de la salida a HTML, a toda entrada del usuario, incluido el título de la reseña. Además, como contramedidas complementarias: poner el atributo HttpOnly en la cookie del ID de sesión para que los scripts no puedan leerla, reprocesar en el servidor las imágenes cargadas para no conservar la secuencia de bytes original, y configurar la Content Security Policy de modo que pueda impedir la ejecución de scripts en línea. Si la empresa Q de este problema hubiera implementado aunque fuera una sola de estas medidas, la cadena del ataque se habría roto en algún punto. Ahora bien, las contramedidas complementarias pueden ser evitadas si el atacante cambia de técnica, así que nunca sustituyen a la solución fundamental del escapado.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.