Latte es sinónimo de seguridad

Latte es el único sistema de plantillas PHP con protección efectiva contra la crítica vulnerabilidad Cross-site Scripting (XSS). Esto es gracias al llamado escape sensible al contexto. Hablemos,

  • cuál es el principio de la vulnerabilidad XSS y por qué es tan peligrosa
  • qué hace que Latte sea tan eficaz en la defensa contra XSS
  • por qué Twig, Blade y otras plantillas pueden ser fácilmente comprometidas

Secuencias de comandos en sitios cruzados (XSS)

Cross-site Scripting (XSS para abreviar) es una de las vulnerabilidades más comunes en los sitios web y una muy peligrosa. Permite a un atacante insertar un script malicioso (llamado malware) en un sitio ajeno que se ejecuta en el navegador de un usuario desprevenido.

¿Qué puede hacer un script de este tipo? Por ejemplo, puede enviar contenido arbitrario desde el sitio comprometido al atacante, incluidos datos sensibles mostrados tras el inicio de sesión. Puede modificar la página o realizar otras peticiones en nombre del usuario. Por ejemplo, si se tratara de un correo web, podría leer mensajes confidenciales, modificar el contenido mostrado o cambiar la configuración, por ejemplo, activar el reenvío de copias de todos los mensajes a la dirección del atacante para obtener acceso a futuros correos electrónicos.

Esta es también la razón por la que XSS encabeza la lista de las vulnerabilidades más peligrosas. Si se descubre una vulnerabilidad en un sitio web, debe eliminarse lo antes posible para evitar su explotación.

¿Cómo surge la vulnerabilidad?

El error se produce en el lugar donde se genera la página web y se imprimen las variables. Imagina que estás creando una página de búsqueda, y al principio habrá un párrafo con el término de búsqueda en el formulario:

echo '<p>Search results for <em>' . $search . '</em></p>';

Un atacante puede escribir cualquier cadena, incluyendo código HTML como <script>alert("Hacked!")</script>en el campo de búsqueda y, por tanto, en la variable $search. Dado que la salida no se desinfecta de ninguna manera, se convierte en parte de la página mostrada:

<p>Search results for <em><script>alert("Hacked!")</script></em></p>

En lugar de mostrar la cadena de búsqueda, el navegador ejecuta JavaScript. Y así el atacante se apodera de la página.

Se podría argumentar que poner código en una variable ejecutará JavaScript, pero sólo en el navegador del atacante. ¿Cómo llega a la víctima? Desde esta perspectiva, podemos distinguir varios tipos de XSS. En nuestro ejemplo de página de búsqueda, estamos hablando de XSS reflejado. En este caso, es necesario engañar a la víctima para que haga clic en un enlace que contiene código malicioso en el parámetro:

https://example.com/?search=<script>alert("Hacked!")</script>

Aunque requiere algo de ingeniería social para hacer que el usuario acceda al enlace, no es difícil. Los usuarios hacen clic en los enlaces, ya sea en correos electrónicos o en las redes sociales, sin pensárselo mucho. Y el hecho de que haya algo sospechoso en la dirección puede enmascararse mediante acortadores de URL, de modo que el usuario solo vea bit.ly/xxx.

Sin embargo, existe una segunda forma de ataque mucho más peligrosa, conocida como stored XSSpersistent XSS, en la que un atacante consigue almacenar código malicioso en el servidor para que se inserte automáticamente en determinadas páginas.

Un ejemplo de esto son los sitios web donde los usuarios publican comentarios. Un atacante envía un post que contiene código y éste se guarda en el servidor. Si el sitio no es lo suficientemente seguro, se ejecutará en el navegador de todos los visitantes.

Parecería que el objetivo del ataque es conseguir que la <script> cadena en la página. De hecho, hay muchas formas de incrustar JavaScript. Tomemos un ejemplo de incrustación utilizando un atributo HTML. Tengamos una galería de fotos en la que se puede insertar un pie de foto a las imágenes, que se imprime en el atributo alt:

echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';

Un atacante sólo tiene que insertar una cadena inteligentemente construida " onload="alert('Hacked!') como etiqueta, y si la salida no es desinfectada, el código resultante tendrá este aspecto:

<img src="photo0145.webp" alt="" onload="alert('Hacked!')">

El falso atributo onload se convierte ahora en parte de la página. El navegador ejecutará el código que contiene en cuanto se descargue la imagen. ¡Hackeado!

¿Cómo defenderse del XSS?

Cualquier intento de detectar un ataque mediante una lista negra, como bloquear la <script> string, etc. son insuficientes. La base de una defensa viable es el saneamiento consistente de todos los datos impresos dentro de la página.

En primer lugar, esto implica sustituir todos los caracteres con significado especial por otras secuencias coincidentes, lo que se denomina escaping en argot (el primer carácter de la secuencia se llama carácter de escape, de ahí el nombre). Por ejemplo, en un texto HTML, el carácter < has a special meaning, which, if it is not to be interpreted as the beginning of a tag, must be replaced by a visually corresponding sequence, the so-called HTML entity &lt;. Y el navegador imprime un carácter

Es muy importante distinguir el contexto en el que se imprimen los datos. Porque los distintos contextos sanean las cadenas de forma diferente. Diferentes caracteres tienen un significado especial en diferentes contextos. Por ejemplo, el escape en texto HTML, en atributos HTML, dentro de algunos elementos especiales, etc. es diferente. Discutiremos esto en detalle en un momento.

Es mejor realizar el escapado directamente cuando la cadena se escribe en la página, asegurándose de que realmente se hace, y se hace sólo una vez. Es mejor si el tratamiento es manejado automáticamente directamente por el sistema de plantillas. Porque si el tratamiento no se hace automáticamente, el programador puede olvidarse de ello. Y una omisión significa que el sitio es vulnerable.

Sin embargo, el XSS no sólo afecta a la salida de datos en las plantillas, sino también a otras partes de la aplicación que deben manejar adecuadamente datos que no son de confianza. Por ejemplo, los JavaScript de su aplicación no deben utilizar innerHTML junto con ellos, sino sólo innerText o textContent. Se debe tener especial cuidado con las funciones que evalúan cadenas como JavaScript, que es eval(), pero también setTimeout(), o usando setAttribute() con atributos de eventos como onload, etc. Pero esto va más allá del ámbito cubierto por las plantillas.

La defensa ideal de 3 puntos:

  1. Reconoce el contexto en el que los datos están siendo emitidos
  2. sanea los datos según las reglas de ese contexto (es decir, “consciente del contexto”)
  3. lo hace automáticamente

Evasión consciente del contexto

¿Qué significa exactamente la palabra contexto? Es un lugar del documento con sus propias reglas de tratamiento de los datos que se van a emitir. Depende del tipo de documento (HTML, XML, CSS, JavaScript, texto plano, …) y puede variar en partes específicas del documento. Por ejemplo, en un documento HTML, hay muchos lugares (contextos) donde se aplican reglas muy diferentes. Le sorprenderá saber cuántas hay. He aquí las cuatro primeras:

<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->

El contexto inicial y básico de una página HTML es el texto HTML. ¿Cuáles son las reglas aquí? Los caracteres de significado especial < and & representan el comienzo de una etiqueta o entidad, por lo que debemos escapar de ellos sustituyéndolos por la entidad HTML (< with &lt;, & with &amp).

El segundo contexto más común es el valor de un atributo HTML. Se diferencia del texto en que aquí el significado especial va a la comilla " or ' que delimita el atributo. Esto debe escribirse como una entidad para que no se vea como el final del atributo. Por otro lado, el carácter &lt; puede utilizarse con seguridad en un atributo porque aquí no tiene ningún significado especial; no puede entenderse como el comienzo de una etiqueta o comentario. Pero cuidado, en HTML se pueden escribir valores de atributo sin comillas, en cuyo caso toda una serie de caracteres tienen un significado especial, por lo que se trata de otro contexto aparte.

Puede que te sorprenda, pero se aplican reglas especiales dentro de las etiquetas <textarea> y <title> donde se utiliza < character need not (but can) be escaped unless followed by /. Pero eso es más bien una curiosidad.

Es interesante dentro de los comentarios HTML. Aquí, las entidades HTML no se utilizan para el escapado. Ni siquiera existe una especificación que indique cómo escapar en los comentarios. Sólo tienes que seguir las un tanto curiosas reglas y evitar ciertas combinaciones de caracteres en ellos.

Los contextos también pueden ser estratificados, lo que ocurre cuando incrustamos JavaScript o CSS en HTML. Esto puede hacerse de dos formas distintas, por elemento o por atributo:

<script>#js-element</script>
<img onclick="#js-attribute">

<style>#css-element</style>
<p style="#css-attribute"></p>

Dos formas y dos tipos diferentes de escapar los datos. Dentro de los elementos <script> y <style> como en el caso de los comentarios HTML, no se realiza el escape mediante entidades HTML. Cuando se escapan datos dentro de estos elementos, sólo hay una regla: el texto no debe contener la secuencia </script y </style respectivamente.

En cambio, los atributos style y on*** se escapan mediante entidades HTML.

Y, por supuesto, dentro de JavaScript o CSS incrustados, se aplican las reglas de escape de esos lenguajes. Así que una cadena en un atributo como onload se escapa primero según las reglas JS y luego según las reglas de atributos HTML.

Uf… Como puedes ver, HTML es un documento muy complejo con capas de contextos, y sin saber exactamente dónde estoy imprimiendo los datos (es decir, en qué contexto), no se sabe cómo hacerlo bien.

¿Quieres un ejemplo?

Pongamos una cadena Rock'n'Roll.

Si la imprimes en texto HTML, no necesitas hacer ninguna sustitución en este caso, porque la cadena no contiene ningún carácter con significado especial. La situación es distinta si la escribes dentro de un atributo HTML entre comillas simples. En este caso, es necesario escapar las comillas a entidades HTML:

<div title='Rock&apos;n&apos;Roll'></div>

Esto era fácil. Una situación mucho más interesante se produce cuando el contexto es en capas, por ejemplo, si la cadena es parte de JavaScript.

Así que primero la escribimos dentro del propio JavaScript. Es decir, la envolvemos entre comillas y al mismo tiempo escapamos las comillas que contiene utilizando el carácter \:

'Rock\'n\'Roll'

Podemos añadir una llamada a una función para que el código haga algo:

alert('Rock\'n\'Roll');

Si insertamos este código en un documento HTML utilizando <script>no necesitamos modificar nada más, porque la secuencia prohibida </script no está presente:

<script> alert('Rock\'n\'Roll'); </script>

Sin embargo, si queremos insertarlo en un atributo HTML, todavía necesitamos escapar las comillas a entidades HTML:

<div onclick='alert(&apos;Rock\&apos;n\&apos;Roll&apos;)'></div>

Sin embargo, el contexto anidado no tiene por qué ser sólo JS o CSS. También suele ser una URL. Los parámetros de las URL se escapan convirtiendo los caracteres especiales en secuencias que empiezan por %. Ejemplo:

https://example.org/?a=Jazz&b=Rock%27n%27Roll

Y cuando imprimimos esta cadena en un atributo, seguimos aplicando el escapado según este contexto y sustituimos & with &amp:

<a href="https://example.org/?a=Jazz&amp;b=Rock%27n%27Roll">

Si has leído hasta aquí, enhorabuena, ha sido agotador. Ahora tienes una buena idea de lo que son los contextos y el escaping. Y no tienes que preocuparte de que sea complicado. Latte lo hace por ti automáticamente.

Latte vs Sistemas Naive

Hemos mostrado cómo escapar correctamente en un documento HTML y lo crucial que es conocer el contexto, es decir, dónde estás imprimiendo los datos. En otras palabras, cómo funciona el escape sensible al contexto. Aunque esto es un prerrequisito para una defensa funcional contra XSS, Latte es el único sistema de plantillas para PHP que lo hace.

¿Cómo es esto posible cuando todos los sistemas hoy en día afirman tener escapado automático? El escape automático sin conocer el contexto es una gilipollez que crea una falsa sensación de seguridad.

Los sistemas de plantillas como Twig, Laravel Blade y otros no ven ninguna estructura HTML en la plantilla. Por lo tanto, tampoco ven contextos. Comparados con Latte, son ciegos e ingenuos. Sólo manejan su propio marcado, todo lo demás es un flujo de caracteres irrelevante para ellos:

░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->

Los sistemas ingenuos se limitan a convertir mecánicamente los caracteres de < > & ' " en entidades HTML, lo cual es una forma válida de escapar en la mayoría de los usos, pero dista mucho de serlo siempre. Por lo tanto, no pueden detectar ni evitar varios agujeros de seguridad, como mostraremos a continuación.

Latte ve la plantilla de la misma manera que tú. Entiende HTML, XML, reconoce etiquetas, atributos, etc. Y por ello, distingue entre contextos y trata los datos en consecuencia. Así que ofrece una protección realmente eficaz contra la crítica vulnerabilidad Cross-site Scripting.

Demostración en directo

A la izquierda puede ver la plantilla en Latte, a la derecha el código HTML generado. La variable $text se muestra varias veces, cada vez en un contexto ligeramente diferente. Y, por lo tanto, se escapa un poco diferente. Usted mismo puede editar el código de la plantilla, por ejemplo cambiar el contenido de la variable, etc. Pruébelo:

{* TRY TO EDIT THIS TEMPLATE *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
- <span>Rock'n'Roll</span>
- <span title='Rock&apos;n&apos;Roll'></span>
- <span title="Rock&apos;n&apos;Roll"></span>
- <img onload="&quot;Rock&apos;n&apos;Roll&quot;">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->

¿No es genial? Latte hace el escape sensible al contexto de forma automática, por lo que el programador:

  • no tiene que pensar o saber cómo escapar datos
  • no puede equivocarse
  • no puede olvidarse de ello

Estos ni siquiera son todos los contextos que Latte distingue a la hora de emitir y para los que personaliza el tratamiento de los datos. Ahora veremos más casos interesantes.

Cómo piratear sistemas ingenuos

Utilizaremos algunos ejemplos prácticos para mostrar lo importante que es la diferenciación de contexto y por qué los sistemas de plantillas ingenuos no proporcionan suficiente protección contra XSS, a diferencia de Latte. Utilizaremos Twig como representante de un sistema ingenuo en los ejemplos, pero lo mismo se aplica a otros sistemas.

Vulnerabilidad de atributos

Intentemos inyectar código malicioso en la página utilizando el atributo HTML como mostramos anteriormente. Tengamos una plantilla en Twig mostrando una imagen:

<img src={{ imageFile }} alt={{ imageAlt }}>

Observa que no hay comillas alrededor de los valores del atributo. El programador puede haberlas olvidado, lo que sucede. Por ejemplo, en React, el código se escribe así, sin comillas, y un programador que está cambiando de lenguaje puede olvidarse fácilmente de las comillas.

El atacante inserta una cadena inteligentemente construida foo onload=alert('Hacked!') como pie de imagen. Ya sabemos que Twig no puede distinguir si una variable se está imprimiendo en un flujo de texto HTML, dentro de un atributo, dentro de un comentario HTML, etc.; en resumen, no distingue entre contextos. Y sólo convierte mecánicamente los caracteres < > & ' " en entidades HTML. Así que el código resultante tendrá este aspecto:

<img src=photo0145.webp alt=foo onload=alert(&#039;Hacked!&#039;)>

¡Se ha creado un agujero de seguridad!

Un atributo onload falso ha pasado a formar parte de la página y el navegador lo ejecuta inmediatamente después de descargar la imagen.

Ahora veamos cómo Latte maneja la misma plantilla:

<img src={$imageFile} alt={$imageAlt}>

Latte ve la plantilla de la misma manera que tú. A diferencia de Twig, entiende HTML y sabe que una variable se imprime como un valor de atributo que no está entre comillas. Por eso las añade. Cuando un atacante inserta la misma leyenda, el código resultante se verá así:

<img src="photo0145.webp" alt="foo onload=alert(&apos;Hacked!&apos;)">

Latte ha evitado con éxito el XSS.

Impresión de una variable en JavaScript

Gracias al escape sensible al contexto, es posible usar variables PHP nativamente dentro de JavaScript.

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Si la variable $movie almacena la cadena 'Amarcord & 8 1/2' genera la siguiente salida. Observe el diferente escapado usado en HTML y JavaScript y también en el atributo onclick:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

Latte comprueba automáticamente si la variable utilizada en los atributos src o href contiene una URL web (es decir, protocolo HTTP) e impide la escritura de enlaces que puedan suponer un riesgo para la seguridad.

{var $link = 'javascript:attack()'}

<a href={$link}>click here</a>

Escribe:

<a href="">click here</a>

La comprobación puede desactivarse mediante un filtro nocheck.

Límites de Latte

Latte no es una protección XSS completa para toda la aplicación. No nos gustaría que se parase a pensar en la seguridad cuando utilice Latte. El objetivo de Latte es asegurar que un atacante no pueda alterar la estructura de una página, manipular elementos HTML o atributos. Pero no comprueba la corrección del contenido de los datos que se emiten. Ni la corrección del comportamiento de JavaScript. Eso está fuera del alcance del sistema de plantillas. Verificar la corrección de los datos, especialmente los introducidos por el usuario y, por tanto, no fiables, es una tarea importante para el programador.

versión: 3.0