Latte es sinónimo de seguridad

Latte es el único sistema de plantillas para PHP con una protección eficaz contra la vulnerabilidad crítica Cross-site Scripting (XSS). Y esto es gracias al llamado escape sensible al contexto. Le contaremos,

  • cuál es el principio de la vulnerabilidad XSS y por qué es tan peligrosa
  • por qué Latte es tan eficaz en la defensa contra XSS
  • cómo se puede crear fácilmente un agujero de seguridad en plantillas Twig, Blade y similares

Cross-site Scripting (XSS)

Cross-site Scripting (abreviado XSS) es una de las vulnerabilidades más comunes de los sitios web y, al mismo tiempo, muy peligrosa. Permite a un atacante insertar un script malicioso (llamado malware) en la página de otra persona, que se ejecuta en el navegador de un usuario desprevenido.

¿Qué puede hacer un script así? Por ejemplo, puede enviar al atacante cualquier contenido de la página atacada, incluidos los datos sensibles mostrados después de iniciar sesión. Puede modificar la página o realizar otras peticiones en nombre del usuario. Si se tratara, por ejemplo, de un webmail, podría leer mensajes sensibles, modificar el contenido mostrado o reconfigurar la configuración, por ejemplo, activar el reenvío de copias de todos los mensajes a la dirección del atacante para obtener acceso también a futuros emails.

Por eso XSS figura en los primeros puestos de los rankings de las vulnerabilidades más peligrosas. Si aparece una vulnerabilidad en un sitio web, es necesario eliminarla lo antes posible para evitar su abuso.

¿Cómo surge la vulnerabilidad?

El error surge en el lugar donde se genera la página web y se imprimen las variables. Imagine que está creando una página con búsqueda, y al principio habrá un párrafo con la expresión buscada en la forma:

echo '<p>Resultados de la búsqueda para <em>' . $search . '</em></p>';

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

<p>Resultados de la búsqueda para <em><script>alert("Hacked!")</script></em></p>

El navegador, en lugar de imprimir la cadena buscada, ejecuta JavaScript. Y así el atacante toma el control de la página.

Puede objetar que al insertar código en la variable, JavaScript se ejecuta, pero solo en el navegador del atacante. ¿Cómo llega a la víctima? Desde esta perspectiva, distinguimos varios tipos de XSS. En nuestro ejemplo con la búsqueda, hablamos de reflected XSS. Aquí también es necesario guiar a la víctima para que haga clic en un enlace que contendrá el código malicioso en el parámetro:

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

Aunque guiar al usuario al enlace requiere cierta ingeniería social, no es nada complicado. Los usuarios hacen clic en los enlaces, ya sea en emails o en redes sociales, sin pensarlo mucho. Y que haya algo sospechoso en la dirección se puede enmascarar usando un acortador de URL, el usuario entonces solo ve bit.ly/xxx.

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

Un ejemplo son las páginas donde los usuarios escriben comentarios. Un atacante envía una publicación que contiene código y este se guarda en el servidor. Si las páginas no están suficientemente aseguradas, se ejecutará en el navegador de cada visitante.

Podría parecer que el núcleo del ataque consiste en introducir la cadena <script> en la página. En realidad, hay muchas formas de insertar JavaScript. Mostraremos, por ejemplo, la inserción mediante un atributo HTML. Supongamos una galería de fotos donde se pueden insertar descripciones a las imágenes, que se imprimen en el atributo alt:

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

Al atacante le basta con insertar como descripción una cadena hábilmente construida " onload="alert('Hacked!') y si la impresión no está saneada, el código resultante se verá así:

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

Ahora forma parte de la página un atributo onload falsificado. El navegador ejecuta el código contenido en él inmediatamente después de descargar la imagen. ¡Hacked!

¿Cómo defenderse de XSS?

Cualquier intento de detectar ataques mediante una blacklist, como bloquear la cadena <script>, etc., es insuficiente. La base de una defensa funcional es la sanitización consistente de todos los datos impresos dentro de la página.

Principalmente se trata de reemplazar todos los caracteres con un significado especial por otras secuencias correspondientes, lo que se llama coloquialmente escape (el primer carácter de la secuencia se llama carácter de escape, de ahí el nombre). Por ejemplo, en el texto HTML, el carácter < tiene un significado especial, que si no debe interpretarse como el inicio de una etiqueta, debemos reemplazarlo por una secuencia visualmente correspondiente, la llamada entidad HTML &lt;. Y el navegador imprime el signo menor que.

Es muy importante distinguir el contexto en el que imprimimos los datos. Porque en diferentes contextos, las cadenas se sanean de manera diferente. En diferentes contextos, diferentes caracteres tienen un significado especial. Por ejemplo, el escape difiere en el texto HTML, en los atributos HTML, dentro de algunos elementos especiales, etc. Lo discutiremos en detalle en un momento.

El saneamiento es mejor realizarlo directamente al imprimir la cadena en la página, asegurando así que realmente se realice y se realice exactamente una vez. Lo mejor es si el saneamiento lo realiza automáticamente el propio sistema de plantillas. Porque si el saneamiento no se realiza automáticamente, el programador puede olvidarlo. Y una omisión significa que el sitio web es vulnerable.

Sin embargo, XSS no solo afecta a la impresión de datos en las plantillas, sino también a otras partes de la aplicación que deben manejar correctamente los datos no confiables. Por ejemplo, es necesario que el JavaScript en su aplicación no use innerHTML en conexión con ellos, sino solo innerText o textContent. Se debe prestar especial atención a las funciones que evalúan cadenas como JavaScript, que es eval(), pero también setTimeout(), o el uso de la función setAttribute() con atributos de evento como onload, etc. Pero esto ya está fuera del área cubierta por las plantillas.

La defensa ideal en 3 puntos:

  1. reconoce el contexto en el que se imprimen los datos
  2. sanea los datos según las reglas del contexto dado (es decir, “sensible al contexto”)
  3. lo hace automáticamente

Escape sensible al contexto

¿Qué se entiende exactamente por la palabra contexto? Es un lugar en el documento con sus propias reglas para el saneamiento de los datos impresos. Depende del tipo de documento (HTML, XML, CSS, JavaScript, texto plano, …) y puede diferir en sus partes específicas. Por ejemplo, en un documento HTML hay muchos lugares (contextos) donde se aplican reglas muy diferentes. Quizás se sorprenda de cuántos hay. Aquí tenemos los primeros cuatro:

<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentář -->

El contexto predeterminado y básico de una página HTML es el texto HTML. ¿Qué reglas se aplican aquí? Los caracteres < y & tienen un significado especial, ya que representan el inicio de una etiqueta o entidad, por lo que debemos escaparlos reemplazándolos por una entidad HTML (< por &lt; & por &amp).

El segundo contexto más común es el valor de un atributo HTML. Se diferencia del texto en que aquí tienen un significado especial las comillas " o ', que delimitan el atributo. Deben escribirse como una entidad para que no se entiendan como el final del atributo. Por el contrario, en el atributo se puede usar de forma segura el carácter <, porque aquí no tiene ningún significado especial, aquí no puede entenderse como el inicio de una etiqueta o comentario. Pero cuidado, en HTML también se pueden escribir valores de atributos sin comillas, en cuyo caso toda una serie de caracteres tienen un significado especial, por lo que se trata de otro contexto independiente.

Quizás le sorprenda, pero se aplican reglas especiales dentro de los elementos <textarea> y <title>, donde el carácter < no necesita (pero puede) escaparse si no va seguido de /. Pero esto es más bien una curiosidad.

Es interesante dentro de los comentarios HTML. Aquí, el escape no se realiza utilizando entidades HTML. De hecho, ninguna especificación indica cómo se debe escapar en los comentarios. Solo es necesario seguir unas reglas curiosas y evitar ciertas combinaciones de caracteres en ellos.

Los contextos también pueden anidarse, lo que ocurre cuando insertamos JavaScript o CSS en HTML. Esto se puede hacer de dos maneras diferentes, con un elemento y con un atributo:

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

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

Dos caminos y dos formas diferentes de escapar datos. Dentro del elemento <script> y <style>, al igual que en el caso de los comentarios HTML, no se realiza el escape mediante entidades HTML. Al imprimir datos dentro de estos elementos, es necesario seguir una única regla: el texto no debe contener la secuencia </script resp. </style.

Por el contrario, en los atributos style y on*** se escapa mediante entidades HTML.

Y, por supuesto, dentro del JavaScript o CSS anidado se aplican las reglas de escape de estos lenguajes. Por lo tanto, una cadena en un atributo, por ejemplo onload, se escapa primero según las reglas de JS y luego según las reglas del atributo HTML.

Uff… Como puede ver, HTML es un documento muy complejo donde se anidan contextos, y sin ser consciente de dónde exactamente estoy imprimiendo los datos (es decir, en qué contexto), no se puede decir cómo hacerlo correctamente.

¿Quiere un ejemplo?

Tomemos la cadena Rock'n'Roll.

Si la imprime en texto HTML, en este caso particular no es necesario realizar ningún reemplazo, porque la cadena no contiene ningún carácter con significado especial. La situación cambia si la imprime dentro de un atributo HTML delimitado por comillas simples. En ese caso, es necesario escapar las comillas a entidades HTML:

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

Esto fue simple. Una situación mucho más interesante surge al anidar contextos, por ejemplo, si la cadena forma parte de JavaScript.

Primero, la imprimimos en el propio JavaScript. Es decir, la envolvemos en comillas y, al mismo tiempo, escapamos las comillas contenidas en ella con el carácter \:

'Rock\'n\'Roll'

También podemos añadir una llamada a alguna función para que el código haga algo:

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

Si insertamos este código en un documento HTML usando <script>, no es necesario modificar nada más, porque no contiene la secuencia prohibida </script:

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

Sin embargo, si quisiéramos insertarlo en un atributo HTML, aún debemos escapar las comillas a entidades HTML:

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

Pero el contexto anidado no tiene por qué ser solo JS o CSS. Comúnmente también es una URL. Los parámetros en una URL se escapan convirtiendo los caracteres con significado especial en secuencias que comienzan con %. Ejemplo:

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

Y cuando imprimimos esta cadena en un atributo, aún aplicamos el escape según este contexto y reemplazamos & por &amp:

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

Si ha leído hasta aquí, felicidades, ha sido exhaustivo. Ahora tiene una buena idea de qué son los contextos y el escape. Y no tiene que preocuparse de que sea complicado. Latte hace esto por usted automáticamente.

Latte vs sistemas ingenuos

Hemos mostrado cómo escapar correctamente en un documento HTML y cuán fundamental es el conocimiento del contexto, es decir, el lugar donde imprimimos los datos. En otras palabras, cómo funciona el escape sensible al contexto. Aunque es un requisito previo necesario para una defensa funcional contra XSS, Latte es el único sistema de plantillas para PHP que puede hacer esto.

¿Cómo es posible, cuando todos los sistemas hoy en día afirman tener escape automático? El escape automático sin conocimiento del contexto es un poco bullshit, que crea una falsa impresió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 los contextos. En comparación con Latte, son ciegos e ingenuos. Solo procesan sus propias etiquetas, todo lo demás es para ellos un flujo de caracteres irrelevante:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- en texto: <span>{{ foo }}</span>
- en etiqueta: <span {{ foo }} ></span>
- en atributo: <span title='{{ foo }}'></span>
- en atributo sin comillas: <span title={{ foo }}></span>
- en atributo que contiene URL: <a href="{{ foo }}"></a>
- en atributo que contiene JavaScript: <img onload="{{ foo }}">
- en atributo que contiene CSS: <span style="{{ foo }}"></span>
- en JavaScript: <script>var = {{ foo }}</script>
- en CSS: <style>body { content: {{ foo }}; }</style>
- en comentario: <!-- {{ foo }} -->

Los sistemas ingenuos solo convierten mecánicamente los caracteres < > & ' " en entidades HTML, lo cual es un método de escape válido en la mayoría de los casos de uso, pero no siempre. Por lo tanto, no pueden detectar ni prevenir el origen de varios agujeros de seguridad, como mostraremos a continuación.

Latte ve la plantilla igual que usted. Entiende HTML, XML, reconoce etiquetas, atributos, etc. Y gracias a esto, distingue los contextos individuales y sanea los datos según ellos. Ofrece así una protección realmente eficaz contra la vulnerabilidad crítica Cross-site Scripting.

░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░░<!--░{$foo}░-->
- en texto: <span>{$foo}</span>
- en etiqueta: <span {$foo} ></span>
- en atributo: <span title='{$foo}'></span>
- en atributo sin comillas: <span title={$foo}></span>
- en atributo que contiene URL: <a href="{$foo}"></a>
- en atributo que contiene JavaScript: <img onload="{$foo}">
- en atributo que contiene CSS: <span style="{$foo}"></span>
- en JavaScript: <script>var = {$foo}</script>
- en CSS: <style>body { content: {$foo}; }</style>
- en comentario: <!-- {$foo} -->

Demostración en vivo

A la izquierda ve la plantilla en Latte, a la derecha está el código HTML generado. La variable $text se imprime varias veces aquí y cada vez en un contexto ligeramente diferente. Y, por lo tanto, también escapada de forma ligeramente diferente. Puede editar el código de la plantilla usted mismo, por ejemplo, cambiar el contenido de la variable, etc. Pruébelo:

{* INTENTA EDITAR ESTA PLANTILLA *}
{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 realiza el escape sensible al contexto automáticamente, por lo que el programador:

  • no tiene que pensar ni saber cómo escapar en cada lugar
  • no puede equivocarse
  • no puede olvidar el escape

Estos ni siquiera son todos los contextos que Latte distingue al imprimir y para los cuales adapta el saneamiento de datos. Ahora repasaremos otros casos interesantes.

Cómo hackear sistemas ingenuos

En varios ejemplos prácticos, mostraremos cuán importante es la distinción de contextos y por qué los sistemas de plantillas ingenuos no proporcionan una protección suficiente contra XSS, a diferencia de Latte. Como representante de un sistema ingenuo, usaremos Twig en las demostraciones, pero lo mismo se aplica a otros sistemas.

Vulnerabilidad por atributo

Intentaremos inyectar código malicioso en la página mediante un atributo HTML, como mostramos anteriormente. Supongamos una plantilla en Twig que renderiza una imagen:

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

Observe que no hay comillas alrededor de los valores de los atributos. El codificador podría haberlas olvidado, lo que simplemente sucede. Por ejemplo, en React, el código se escribe así, sin comillas, y un codificador que alterna lenguajes puede olvidarlas fácilmente.

Un atacante inserta como descripción de la imagen una cadena hábilmente construida foo onload=alert('Hacked!'). Ya sabemos que Twig no puede saber si la variable se imprime en el flujo de texto HTML, dentro de un atributo, comentario HTML, etc., en resumen, no distingue contextos. Y solo convierte mecánicamente los caracteres < > & ' " en entidades HTML. Así que el código resultante se verá así:

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

¡Y se ha creado un agujero de seguridad!

Ahora forma parte de la página un atributo onload falsificado 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 igual que usted. A diferencia de Twig, entiende HTML y sabe que la variable se imprime como el valor de un atributo que no está entre comillas. Por eso las añade. Cuando un atacante inserta la misma descripción, el código resultante se verá así:

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

Latte previno exitosamente el XSS.

Impresión de variable en JavaScript

Gracias al escape sensible al contexto, es posible usar variables PHP de forma completamente nativa dentro de JavaScript.

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

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

Si la variable $movie contiene la cadena 'Amarcord & 8 1/2', se generará la siguiente salida. Observe que dentro de HTML se usa un escape diferente al que se usa dentro de JavaScript y aún otro diferente 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>

Comprobación de enlaces

Latte comprueba automáticamente si la variable utilizada en los atributos src o href contiene una URL web (es decir, protocolo HTTP) y evita la impresión de enlaces que puedan suponer un riesgo de seguridad.

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

<a href={$link}>haz clic</a>

Imprime:

<a href="">haz clic</a>

La comprobación se puede desactivar con el filtro nocheck.

Límites de Latte

Latte no es una protección completamente completa contra XSS para toda la aplicación. No nos gustaría que dejara de pensar en la seguridad al usar Latte. El objetivo de Latte es asegurar que un atacante no pueda modificar la estructura de la página, falsificar elementos o atributos HTML. Pero no controla la corrección del contenido de los datos impresos. Ni la corrección del comportamiento de JavaScript. Esto ya está fuera de la competencia del sistema de plantillas. La verificación de la corrección de los datos, especialmente los insertados por el usuario y, por lo tanto, no confiables, es una tarea importante del programador.

versión: 3.0