Quiz: ¿puede defenderse de la vulnerabilidad XSS?
¡Pon a prueba tus conocimientos en el quiz de seguridad! ¿Puede evitar que un atacante tome el control de la página HTML?

En todas las tareas resolverá la misma pregunta: cómo imprimir
correctamente la variable $str
en la página HTML para que no surja
una vulnerabilidad XSS. La
base de la defensa es el escape, lo que significa reemplazar caracteres
con significado especial por las secuencias correspondientes. Por ejemplo, al
imprimir una cadena en texto HTML, donde el carácter <
tiene un
significado especial (señala el inicio de una etiqueta), lo reemplazamos por la
entidad HTML <
y el navegador mostrará correctamente el
símbolo <
.
Tenga cuidado, porque la vulnerabilidad XSS es muy grave. Puede hacer que un atacante tome el control de la página o incluso de la cuenta de usuario. ¡Mucho éxito y que logre mantener segura la página HTML!
El primer trío de preguntas
Indique qué caracteres y de qué manera es necesario tratar en el primer, segundo y tercer ejemplo:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Si la salida no se tratara de ninguna manera, se convertiría en parte de la
página mostrada. Si un atacante introdujera en la variable la cadena
'foo" onclick="evilCode()'
y la salida no se tratara, provocaría
que al hacer clic en el elemento se ejecutara su código:
$str = 'foo" onclick="evilCode()'
❌ sin tratar: <input value="foo" onclick="evilCode()">
✅ con tratamiento: <input value="foo" onclick="evilCode()">
Solución de los ejemplos individuales:
- los caracteres
<
y&
representan el inicio de una etiqueta HTML y entidades, los reemplazamos por<
y&
- los caracteres
"
y&
representan el final del valor del atributo y el inicio de una entidad HTML, los reemplazamos por"
y&
- los caracteres
'
y&
representan el final del valor del atributo y el inicio de una entidad HTML, los reemplazamos por'
y&
Por cada respuesta correcta obtiene un punto. Por supuesto, en los tres casos se pueden reemplazar también otros caracteres por entidades, no pasa nada, pero no es necesario.
Pregunta n.º 4
Continuamos. ¿Qué caracteres es necesario reemplazar al imprimir la variable en este contexto?
<input value=<?= $str ?>>
Solución: Como puede ver, aquí faltan las comillas. Lo más fácil es
simplemente añadir las comillas y luego escapar igual que en la pregunta
anterior. Existe una segunda solución, que es reemplazar en la cadena por
entidades HTML el espacio y todos los caracteres que tienen un significado
especial dentro de la etiqueta, es decir, >
, /
,
=
y algunos
otros.
Pregunta n.º 5
Ahora empieza a ser más interesante. ¿Qué caracteres es necesario tratar en este contexto?
<script>
let foo = '<?= $str ?>';
</script>
Solución: Dentro de la etiqueta <script>
, las reglas para
el escape las determina JavaScript. Las entidades HTML no se usan aquí,
sin embargo, aplica una regla especial. Entonces, ¿qué caracteres escapamos?
Dentro de una cadena de JavaScript, escapamos por supuesto el carácter
'
, que la delimita, usando una barra inclinada, es decir, lo
reemplazamos por \'
. Dado que JavaScript no soporta cadenas
multilínea (solo como template
literal), debemos escapar también los caracteres de fin de línea. Pero
cuidado, además de los caracteres habituales \n
y \r
,
JavaScript considera como fines de línea también los caracteres unicode
\u2028
y \u2029
, que debemos escapar también. Y
finalmente, la regla especial mencionada: en la cadena no debe aparecer
</script
. Esto se puede prevenir, por ejemplo, reemplazándolo
por <\/script
.
Si sabía esto, felicidades.
Pregunta n.º 6
El siguiente contexto parece solo una variación del anterior. ¿Cree que el tratamiento será diferente?
<p onclick="foo('<?= $str ?>')"></p>
Solución: Aquí también aplican las reglas para el escape en cadenas de
JavaScript, pero a diferencia del contexto anterior, donde no se escapaba
mediante entidades HTML, aquí sí se escapa. Es decir, primero escapamos la
cadena de JavaScript usando barras inclinadas y luego reemplazamos los
caracteres con significado especial ("
y &
) por
entidades HTML. ¡Cuidado, el orden correcto es importante!
Como puede ver, ¡el mismo literal de JavaScript puede estar codificado de
manera diferente en el elemento <script>
y de manera
diferente en el atributo!
Pregunta n.º 7
Volvemos de JavaScript a HTML. ¿Qué caracteres debemos reemplazar dentro del comentario y de qué manera?
<!-- <?= $str ?> -->
Solución: dentro de un comentario HTML (y XML), todos los caracteres
especiales tradicionales, como <
, &
,
"
y '
, pueden aparecer. Lo que está prohibido, y esto
quizás le sorprenda, es el par de caracteres --
. El escape de esta
secuencia no está especificado, así que depende de usted cómo la reemplace.
Puede intercalar espacios. O quizás reemplazarla por ==
.
Pregunta n.º 8
Ya nos acercamos al final, así que intentaremos variar la pregunta. Intente reflexionar sobre qué hay que tener en cuenta al imprimir la variable en este contexto:
<a href="<?= $str ?>">...</a>
Solución: además del escape, es importante verificar también que la URL no
contenga un esquema peligroso como javascript:
, porque una URL
construida de esta manera llamaría al código del atacante al hacer clic.
Pregunta n.º 9
Para terminar, una joya para los verdaderos entendidos. Es una muestra de una
aplicación que utiliza un framework de JavaScript moderno, concretamente Vue.
A ver si se le ocurre qué hay que tener en cuenta al imprimir la variable
dentro del elemento #app
:
<div id="app">
<?= $str ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
...
})
</script>
Este código crea una aplicación Vue que se renderizará en el elemento
#app
. Vue entiende el contenido de este elemento como su plantilla.
Y dentro de la plantilla interpreta
las llaves dobles, que significan la impresión de una variable o la llamada de
código JavaScript (p.ej., {{ foo }}
).
Por lo tanto, dentro del elemento #app
, además de los
caracteres <
y &
, también tiene un significado
especial el par {{
, que debemos reemplazar por otra secuencia
correspondiente para que Vue no lo interprete como su etiqueta. Sin embargo, el
reemplazo por entidades HTML no ayuda en este caso. ¿Cómo solucionarlo?
Funciona un truco: entre las llaves insertamos un comentario HTML vacío
{<!-- -->{
y Vue ignora tal secuencia.
Resultados del quiz
¿Cómo le fue en el quiz? ¿Cuántas respuestas correctas tiene? Si respondió correctamente al menos a 4 preguntas, pertenece al 8% de los mejores participantes. ¡Felicidades!
Sin embargo, para garantizar la seguridad de su sitio web, es esencial tratar correctamente la salida en todas las situaciones.
Si le sorprendió cuántos contextos diferentes pueden aparecer en una página HTML común, debe saber que no mencionamos ni de lejos todos. Eso sería un quiz mucho más largo. Sin embargo, no necesita ser un experto en escape en cada contexto si su sistema de plantillas puede manejarlo.
Así que vamos a probarlos.
¿Cómo les va a los sistemas de plantillas?
Todos los sistemas de plantillas modernos presumen de la función de autoescaping, que escapa automáticamente todas las variables impresas. Si lo hacen correctamente, su sitio web está seguro. Si lo hacen mal, el sitio web está expuesto al riesgo de vulnerabilidad XSS con todas sus graves consecuencias.
Probaremos los sistemas de plantillas populares de las preguntas de este quiz para averiguar cuán efectivo es su autoescaping. Comienza la prueba comparativa de sistemas de plantillas para PHP.
Twig ❌
El primero en la lista es el sistema de plantillas Twig (versión 3.5), que se usa más
comúnmente en combinación con el framework Symfony. Le daremos la tarea de
responder a todas las preguntas del quiz. La variable $str
siempre
estará llena de una cadena engañosa y veremos cómo maneja su impresión. Los
resultados los ve a la derecha. También puede explorar en el playground
sus respuestas y comportamiento.
{% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>
{% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>
{% set str = "'\"\n\u{2028}" %}
5) <script> let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>
{% set str = "-- ---" %}
7) <!-- {{ str }} -->
{% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>
{% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"u{2028})"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
¡Twig falló en seis de nueve pruebas!
Desafortunadamente, el escape automático de Twig funciona solo en texto y atributos HTML, y además solo si están encerrados entre comillas. Tan pronto como faltan las comillas, Twig no informa ningún error y crea un agujero de seguridad XSS.
Esto es particularmente desagradable porque así es como se escriben los valores de los atributos en librerías populares como React o Svelte. Un programador que usa simultáneamente Twig y React puede olvidar las comillas de forma bastante natural.
El autoescaping de Twig falla también en todos los demás ejemplos. En los
contextos (5) y (6) es necesario escapar manualmente usando
{{ str|escape('js') }}
, para otros contextos Twig ni siquiera
ofrece una función de escape. Tampoco dispone de protección contra la
impresión de un enlace malicioso (8) ni soporte para plantillas de
Vue (9).
Blade ❌❌
El segundo participante es el sistema de plantillas Blade (versión 10.9), que está estrechamente integrado con Laravel y su ecosistema. Nuevamente verificaremos sus capacidades en nuestras preguntas del quiz. También puede explorar en el playground sus respuestas.
@php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>
@php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>
@php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>
@php($str = "-- ---")
7) <!-- {{ $str }} -->
@php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>
@php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </div>
¡Blade falló en seis de nueve pruebas!
El resultado es similar al de Twig. Nuevamente aplica que el escape
automático funciona solo en texto y atributos HTML y solo si están encerrados
entre comillas. El autoescaping de Blade falla también en todos los demás
ejemplos. En los contextos (5) y (6) es necesario escapar manually usando
{{ Js::from($str) }}
. Para otros contextos Blade ni siquiera ofrece
una función de escape. No dispone de protección contra la impresión de un
enlace malicioso (8) ni soporte para plantillas de Vue (9).
Lo que es sorprendente, sin embargo, es el fallo de la directiva
@php
en Blade, lo que provoca la impresión del propio código PHP
directamente en la salida, como se ve en la última línea.
Smarty ❌❌❌
Ahora probaremos el sistema de plantillas más antiguo para PHP, que es Smarty (versión 4.3). Para gran sorpresa,
este sistema no tiene activo el escape automático. Por lo tanto, al imprimir
variables, debe indicar cada vez el filtro {$var|escape}
,
o activar el escape automático de HTML. La información sobre esto está
bastante escondida en la documentación.
{$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{$str = "foo onclick=evilCode()"}
4) <input value={$str}>
{$str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{$str = "-- ---"}
7) <!-- {$str} -->
{$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\u2028)"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
¡Smarty falló en seis de nueve pruebas!
El resultado es a primera vista similar al de las librerías anteriores.
Smarty solo puede escapar automáticamente en texto y atributos HTML, y solo
cuando los valores están encerrados entre comillas. En todos los demás casos
falla. En los contextos (5) y (6) es necesario escapar manualmente usando
{$str|escape:javascript}
. Pero esto solo es posible si el escape
automático de HTML no está activo, de lo contrario, estos escapes entran en
conflicto entre sí. Smarty es, por lo tanto, desde el punto de vista de la
seguridad, el fracaso absoluto de esta prueba.
Latte ✅
El trío lo cierra el sistema de plantillas Latte (versión 3.0). Probaremos su autoescaping. También puede explorar en el playground sus respuestas y comportamiento.
{var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{var $str = "foo onclick=evilCode()"}
4) <input value={$str}>
{var $str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{var $str = "-- ---"}
7) <!-- {$str} -->
{var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></p>
✅ <!-- - - - - - -->
✅ <a href="">...</a>
✅ <div id="app"> {<!-- -->{ foo }} </div>
¡Latte sobresalió en las nueve tareas!
Logró manejar la falta de comillas en los atributos HTML, procesar
JavaScript tanto en el elemento <script>
como en los
atributos, y lidiar con la secuencia prohibida en los comentarios HTML.
Además, previno la situación en la que hacer clic en un enlace falsificado por un atacante podría ejecutar su código. Y logró manejar el escape de etiquetas para Vue.
Prueba de bonificación
Una de las capacidades esenciales de todos los sistemas de plantillas es el
trabajo con bloques y la herencia de plantillas asociada. Por lo tanto,
intentaremos dar a todos los sistemas de plantillas probados una tarea más.
Crearemos un bloque description
que imprimiremos en un atributo
HTML. En el mundo real, por supuesto, la definición del bloque se encontraría
en la plantilla hija y su impresión en la plantilla padre, es decir, por
ejemplo, en el layout. Esta es solo una forma simplificada, pero suficiente para
probar el autoescaping al imprimir bloques. ¿Cómo les fue?
Twig: falló ❌ al imprimir bloques no trata los caracteres
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade: falló ❌ al imprimir bloques no trata los caracteres
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte: pasó ✅ al imprimir bloques trató correctamente los caracteres problemáticos
{block description}
rock n' roll
{/block}
<meta name='description'
content='{include description}'>
<meta name='description'
content=' rock n' roll '> ✅
¿Por qué tantos sitios web son vulnerables?
El autoescaping en sistemas como Twig, Blade o Smarty funciona simplemente
reemplazando cinco caracteres <>"'&
por entidades HTML y
no distingue el contexto de ninguna manera. Por eso funciona solo en algunas
situaciones y falla en todas las demás. El autoescaping ingenuo es una
función peligrosa porque crea una falsa sensación de seguridad.
Por lo tanto, no es sorprendente que actualmente más del 27% de los sitios web tengan vulnerabilidades críticas, principalmente XSS (fuente: Acunetix Web Vulnerability Report). ¿Cómo salir de esto? Usar un sistema de plantillas que distinga contextos.
Latte es el único sistema de plantillas en PHP que no percibe la plantilla solo como una cadena de caracteres, sino que entiende HTML. Comprende qué son las etiquetas, los atributos, etc. Distingue contextos. Y por eso escapa correctamente en texto HTML, de manera diferente dentro de una etiqueta HTML, de manera diferente dentro de JavaScript, etc.
Latte representa así el único sistema de plantillas seguro.
Además, gracias a su comprensión de HTML, ofrece los maravillosos n:attributes, que encantan a los usuarios:
<ul n:if="$menu">
<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>
Para enviar un comentario, inicie sesión