Cuestionario: ¿Puede defenderse de una vulnerabilidad XSS?

hace 2 años por David Grudl  

Ponga a prueba sus conocimientos y habilidades de seguridad en este cuestionario. ¿Puede evitar que un atacante tome el control de una página HTML?

En todas las tareas, abordarás la misma cuestión: cómo mostrar correctamente la variable $str en una página HTML sin crear una vulnerabilidad XSS. La base de la defensa es escaping, que significa reemplazar caracteres con significados especiales por las secuencias correspondientes. Por ejemplo, al pasar una cadena a texto HTML, en la que el carácter < tiene un significado especial (indica el comienzo de una etiqueta), lo sustituimos por la entidad HTML &lt;, y el navegador muestra correctamente el símbolo <.

Esté atento, ya que la vulnerabilidad XSS es muy grave. Puede hacer que un atacante tome el control de una página o incluso de la cuenta de un usuario. Buena suerte y ¡que consigas mantener la página HTML segura!

El primer trío de preguntas

Especifique qué caracteres deben tratarse y cómo en los ejemplos primero, segundo y tercero:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Si la salida no se tratara de ninguna manera, pasaría a formar parte de la página mostrada. Si un atacante consiguiera insertar la cadena 'foo" onclick="evilCode()' en una variable y la salida no se tratara, provocaría que su código se ejecutara al hacer clic en el elemento:

$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated:     <input value="foo&quot; onclick=&quot;evilCode()">

Soluciones para cada ejemplo:

  1. los caracteres < y & representan el principio de una etiqueta y una entidad HTML; sustitúyalos por &lt; y &amp;
  2. los caracteres " y & representan el final de un valor de atributo y el principio de una entidad HTML; sustitúyalos por &quot; y &amp;
  3. los caracteres ' y & representan el final de un valor de atributo y el principio de una entidad HTML; sustitúyalos por &apos; y &amp;

Obtendrá un punto por cada respuesta correcta. Por supuesto, en los tres casos también puedes sustituir otros caracteres por entidades; no causa ningún perjuicio, pero no es necesario.

Pregunta nº 4

A continuación, ¿qué caracteres deben sustituirse al visualizar una variable en este contexto?

<input value=<?= $str ?>>

Solución: Como puede ver, aquí faltan las comillas. La forma más fácil es simplemente añadir las comillas y luego escapar como en la pregunta anterior. También hay una segunda solución, que consiste en sustituir los espacios y todos los caracteres que tengan un significado especial dentro de una etiqueta, como >, /, =, y algunos otros por entidades HTML.

Pregunta nº 5

Ahora se pone más interesante. Qué personajes hay que tratar en este contexto:

<script>
	let foo = '<?= $str ?>';
</script>

Solución: Dentro de la etiqueta <script> las reglas de escape están determinadas por JavaScript. Aquí no se utilizan entidades HTML, pero hay una regla especial. ¿Qué caracteres escapamos? Dentro de la cadena JavaScript, naturalmente escapamos el carácter ' que la delimita, utilizando una barra invertida, sustituyéndolo por \'. Dado que JavaScript no admite cadenas de varias líneas (excepto como literales de plantilla), también debemos escapar los caracteres de nueva línea. Sin embargo, tenga en cuenta que además de los caracteres habituales \n y \r, JavaScript también considera los caracteres Unicode \u2028 y \u2029 como caracteres de nueva línea, que también debemos escapar. Por último, la regla especial mencionada: la cadena no debe contener </script. Esto puede evitarse, por ejemplo, sustituyéndolo por <\/script.

Si sabías esto, enhorabuena.

Pregunta nº 6

El siguiente contexto parece ser sólo una variación del anterior. ¿Cree que el tratamiento será diferente?

<p onclick="foo('<?= $str ?>')"></p>

Solución: De nuevo, aquí se aplican las reglas de escape para cadenas JavaScript, pero a diferencia del contexto anterior en el que las entidades HTML no se escapaban, aquí sí se escapan. Así pues, primero escapamos la cadena JavaScript utilizando barras invertidas y luego sustituimos los caracteres especiales (" y &) por entidades HTML. Tenga cuidado, el orden correcto es importante.

Como puedes ver, el mismo literal JavaScript puede codificarse de forma diferente en un elemento <script> y de forma distinta en un atributo.

Pregunta nº 7

Volvamos de JavaScript a HTML. ¿Qué caracteres necesitamos reemplazar dentro del comentario y cómo?

<!-- <?= $str ?> -->

Solución: Dentro de un comentario HTML (y XML) pueden aparecer todos los caracteres especiales tradicionales, como <, &, " y '. Lo que está prohibido, y esto puede sorprenderle, es el par de caracteres --. No se especifica la forma de escapar de esta secuencia, por lo que depende de usted cómo reemplazarla. Puede intercalarlos con espacios. O, por ejemplo, sustituirlos por ==.

Pregunta nº 8

Nos acercamos al final, así que vamos a intentar variar la pregunta. Intenta pensar en lo que debes tener cuidado al imprimir una variable en este contexto:

<a href="<?= $str ?>">...</a>

Solución: Además de escapar, también es importante verificar que la URL no contiene un esquema peligroso como javascript:, porque una URL compuesta así ejecutaría el código del atacante al hacer clic.

Pregunta nº 9

Por último, una delicia para los verdaderos entendidos. Este es un ejemplo de una aplicación que utiliza un framework JavaScript moderno, concretamente Vue. A ver si te enteras de qué hay que tener cuidado al imprimir una 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 interpreta el contenido de este elemento como su plantilla. Y dentro de la plantilla, interpreta las llaves dobles, que representan la salida de variables o la llamada a código JavaScript (por ejemplo, {{ foo }}).

Así, dentro del elemento #app, además de los caracteres < y &, el par {{ también tiene un significado especial, que necesitamos reemplazar con otra secuencia apropiada para evitar que Vue lo interprete como su propia etiqueta. Reemplazar con entidades HTML no ayuda en este caso. ¿Cómo solucionarlo? Hay un truco: inserta un comentario HTML vacío entre las llaves {<!-- -->{y Vue ignorará esta secuencia.

Resultados

¿Cómo te ha ido en el test? ¿Cuántas respuestas correctas tiene? Si has respondido correctamente al menos a 4 preguntas, te encuentras entre el 8% de los que más han resuelto: ¡enhorabuena!

Sin embargo, para garantizar la seguridad de su sitio web es necesario gestionar correctamente la salida en todas las situaciones.

Si le ha sorprendido la cantidad de contextos diferentes que pueden aparecer en una página HTML típica, sepa que no los hemos mencionado todos ni mucho menos. Eso alargaría mucho el cuestionario. Sin embargo, no tienes que ser un experto en escapes en cada contexto si tu sistema de plantillas puede manejarlo.

Así que, vamos a probarlos.

¿Cómo funcionan los sistemas de plantillas?

Todos los sistemas de plantillas modernos disponen de una función autoescaping que escapa automáticamente todas las variables de salida. Si lo hacen correctamente, su sitio web está a salvo. Si lo hacen mal, el sitio está expuesto al riesgo de vulnerabilidad XSS con todas sus graves consecuencias.

Probaremos sistemas de plantillas populares a partir de las preguntas de este cuestionario para determinar la efectividad de su auto-escapado. Que comience la revisión de los sistemas de plantillas PHP.

Twig ❌

En primer lugar está el sistema de plantillas Twig (versión 3.5), más comúnmente utilizado junto con el framework Symfony. Le encargaremos que responda a todas las preguntas del cuestionario. La variable $str se rellenará siempre con una cadena engañosa, y veremos cómo gestiona su salida. Puedes ver los resultados a la derecha. También puedes explorar sus respuestas y su comportamiento en el patio de recreo.

   {% 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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;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 sólo funciona en texto y atributos HTML, e incluso entonces sólo cuando están entre comillas. En cuanto faltan las comillas, Twig no informa de 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 utilice tanto Twig como React puede olvidarse de las comillas con total naturalidad.

El autoescapado de Twig también falla en todos los demás ejemplos. En los contextos (5) y (6), el escapado manual es necesario usando {{ str|escape('js') }}mientras que para otros contextos, Twig ni siquiera ofrece una función de escape. También carece de protección contra la impresión de un enlace malicioso (8) o soporte para plantillas 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. De nuevo, pondremos a prueba sus capacidades en las preguntas de nuestro cuestionario. También puedes explorar sus respuestas en el patio de recreo.

   @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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </div>

¡La cuchilla falló en seis de nueve pruebas!

El resultado es similar al de Twig. De nuevo, el escapado automático sólo funciona en texto y atributos HTML y sólo si están entre comillas. El autoescapado de Blade también falla en todos los demás ejemplos. En los contextos (5) y (6), es necesario el escapado manual utilizando {{ Js::from($str) }}. Para otros contextos, Blade ni siquiera ofrece una función de escape. También carece de protección contra la impresión de un enlace malicioso (8) o de soporte para plantillas Vue (9).

Sin embargo, lo que sorprende es el fallo de la directiva @php en Blade, que provoca la salida de su propio código PHP directamente a la salida, como se ve en la última línea.

Latte ✅

El trío se cierra con el sistema de plantillas Latte (versión 3.0). Probaremos su autoescapado. También puedes explorar sus respuestas y comportamiento en el patio de recreo.

   {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>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></p>


✅ <!--  - -  - - -  -->


✅ <a href="">...</a>


✅ <div id="app"> {<!-- -->{ foo }} </div>

¡Latte destacó en las nueve tareas!

Consiguió manejar las comillas que faltaban en los atributos HTML, procesó JavaScript tanto en el elemento <script> como en los atributos, y trató la secuencia prohibida en los comentarios HTML.

Además, evitó que al hacer clic en un enlace malicioso proporcionado por un atacante se ejecutara su código. Y logró manejar el escape de etiquetas para Vue.

Prueba adicional

Una de las capacidades esenciales de todos los sistemas de plantillas es trabajar con bloques y la herencia de plantillas relacionada. Por lo tanto, daremos 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, la definición del bloque se encontraría, por supuesto, en la plantilla hija y su salida en la plantilla padre, como el diseño. Esto es sólo una forma simplificada, pero es suficiente para probar el autoescapado cuando se imprimen bloques. ¿Qué tal funciona?

Twig: fallo ❌ en la salida de bloques, los caracteres no se escapan correctamente.

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Blade: fallo ❌ al dar salida a bloques, los caracteres no se escapan correctamente.

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte: pasado ✅ al dar salida a bloques, manejaba correctamente los caracteres problemáticos.

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

¿Por qué son vulnerables tantos sitios web?

El autoescapado en sistemas como Twig, Blade o Smarty funciona simplemente sustituyendo cinco caracteres <>"'& por entidades HTML y no distingue el contexto. Por lo tanto, sólo funciona en algunas situaciones y falla en todas las demás. El autoescapado ingenuo es una función peligrosa porque crea una falsa sensación de seguridad.

No es de extrañar, por tanto, 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 esta situación? Utilice un sistema de plantillas que distinga los contextos.

Latte es el único sistema de plantillas PHP que no percibe una plantilla como una simple cadena de caracteres, sino que entiende HTML. Sabe lo que son las etiquetas, los atributos, etc. Distingue contextos. Y por tanto, escapa correctamente en texto HTML, de forma diferente dentro de etiquetas HTML, de forma 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>