Manipulación segura de DOM con la API de Sanitizer

El objetivo de la nueva API de Sanitizer es compilar un procesador sólido para que las cadenas arbitrarias se inserten de forma segura en una página.

Jack J
Jack J

Las aplicaciones trabajan con cadenas que no son de confianza todo el tiempo, pero renderizar de forma segura ese contenido como parte de un documento HTML puede ser complicado. Si no se tiene el cuidado suficiente, es fácil crear accidentalmente oportunidades de secuencias de comandos entre sitios (XSS) que los atacantes maliciosos podrían aprovechar.

Para mitigar ese riesgo, la nueva propuesta de la API de Sanitizer busca compilar un procesador sólido para que las cadenas arbitrarias se inserten de forma segura en una página. En este artículo, se presenta la API y se explica su uso.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

Cómo escapar la entrada del usuario

Cuando se insertan entradas del usuario, cadenas de consulta, contenido de cookies, etc., en el DOM, las cadenas deben tener los caracteres de escape correctos. Se debe prestar especial atención a la manipulación del DOM a través de .innerHTML, donde las cadenas sin escape son una fuente típica de XSS.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Si escapas los caracteres especiales HTML en la cadena de entrada anterior o la expandes con .textContent, no se ejecutará alert(0). Sin embargo, dado que el usuario <em> agrega el usuario también se expande como una cadena tal como es, este método no se puede usar para mantener la decoración de texto en HTML.

Lo mejor que se puede hacer aquí no es escapar, sino limpiarlo.

Limpia las entradas del usuario

Cuál es la diferencia entre el escape y la limpieza

Este tipo de escape hace referencia al reemplazo de caracteres HTML especiales con entidades HTML.

La limpieza hace referencia a quitar las partes semánticamente dañinas (como la ejecución de secuencias de comandos) de las cadenas HTML.

Ejemplo

En el ejemplo anterior, <img onerror> hace que se ejecute el controlador de errores, pero si se quitara el controlador onerror, sería posible expandirlo de forma segura en el DOM y dejar <em> intacto.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Para realizar una limpieza correcta, es necesario analizar la cadena de entrada como HTML, omitir las etiquetas y los atributos que se consideren perjudiciales, y conservar los inofensivos.

La especificación propuesta de la API de Sanitizer tiene como objetivo proporcionar procesamiento como una API estándar para navegadores.

API de Sanitizer

La API de Sanitizer se usa de la siguiente manera:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

Sin embargo, { sanitizer: new Sanitizer() } es el argumento predeterminado. Puede ser como se muestra a continuación.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Vale la pena señalar que setHTML() se define en Element. Como es un método de Element, el contexto que se analizará es evidente (en este caso, <div>), el análisis se realiza una vez de forma interna y el resultado se expande directamente al DOM.

Para obtener el resultado de la limpieza como una cadena, puedes usar .innerHTML de los resultados de setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Personalización mediante configuración

La API de Sanitizer está configurada de forma predeterminada para quitar las cadenas que podrían activar la ejecución de secuencias de comandos. Sin embargo, también puedes agregar tus propias personalizaciones al proceso de limpieza a través de un objeto de configuración.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Las siguientes opciones especifican cómo el resultado de la limpieza debe tratar el elemento especificado.

allowElements: Son los nombres de los elementos que debe retener el desinfectante.

blockElements: Son los nombres de los elementos que el desinfectante debe quitar y, al mismo tiempo, conservar sus elementos secundarios.

dropElements: Son los nombres de los elementos que el desinfectante debe quitar, junto con sus elementos secundarios.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

También puedes controlar si la limpieza permitirá o rechazará atributos específicos con las siguientes opciones:

  • allowAttributes
  • dropAttributes

Las propiedades allowAttributes y dropAttributes esperan listas de coincidencias de atributos: los objetos cuyas claves son nombres de atributos y los valores son listas de elementos de destino o el comodín *.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements es la opción para permitir o rechazar elementos personalizados. Si están permitidos, se siguen aplicando otras configuraciones para los elementos y atributos.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

Superficie de la API

Comparación con DomPurify

DOMPurify es una biblioteca conocida que ofrece funcionalidad de limpieza. La principal diferencia entre la API de Sanitizer y DOMPurify es que DOMPurify muestra el resultado de la limpieza como una cadena, que debes escribir en un elemento DOM a través de .innerHTML.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify puede servir como un resguardo cuando la API de Sanitizer no está implementada en el navegador.

La implementación de DOMPurify presenta algunas desventajas. Si se devuelve una cadena, DOMPurify y .innerHTML analizan la cadena de entrada dos veces. Este análisis doble desperdicia tiempo de procesamiento, pero también puede generar vulnerabilidades interesantes debido a casos en los que el resultado del segundo análisis es diferente del primero.

HTML también necesita analizar el contexto. Por ejemplo, <td> tiene sentido en <table>, pero no en <div>. Dado que DOMPurify.sanitize() solo toma una cadena como argumento, el contexto de análisis tenía que adivinarse.

La API de Sanitizer mejora el enfoque de DOMPurify y está diseñada para eliminar la necesidad de doble análisis y aclarar el contexto de análisis.

Estado de la API y compatibilidad con el navegador

La API de Sanitizer se está debatiendo en el proceso de estandarización y Chrome está en proceso de implementarla.

Step Estado
1. Crear explicación Completo
2. Crear borrador de especificación Completo
3. Recopila comentarios e itera en el diseño Completo
4. Prueba de origen de Chrome Completo
5. Lanzamiento Intent de envío en M105

Mozilla: Considera que esta propuesta vale la pena crear un prototipo y la implementa de forma activa.

WebKit: Consulta la respuesta en la lista de distribución de WebKit.

Cómo habilitar la API de Sanitizer

Navegadores compatibles

  • x
  • x
  • x

Origen

Se habilita mediante la opción about://flags o CLI

Chrome

Chrome está en proceso de implementar la API de Sanitizer. En Chrome 93 o versiones posteriores, puedes probar el comportamiento si habilitas la marca about://flags/#enable-experimental-web-platform-features. En versiones anteriores de Chrome Canary y el canal para desarrolladores, puedes habilitarlo a través de --enable-blink-features=SanitizerAPI y probarlo ahora mismo. Consulta las instrucciones para ejecutar Chrome con funciones experimentales.

Firefox

Firefox también implementa la API de Sanitizer como función experimental. Para habilitarlo, establece la marca dom.security.sanitizer.enabled en true en about:config.

Detección de funciones

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Comentarios

Si pruebas esta API y tienes comentarios, nos encantaría recibirlos. Comparte tus opiniones sobre los problemas de GitHub de la API de Sanitizer y analízalos con los autores de especificaciones y las personas interesadas en esta API.

Si detectas errores o comportamientos inesperados en la implementación de Chrome, informa un error. Selecciona los componentes de Blink>SecurityFeature>SanitizerAPI y comparte detalles para ayudar a los implementadores a realizar un seguimiento del problema.

Demostración

Para ver la API de Sanitizer en acción, consulta el Playground de la API de Sanitizer de Mike West:

Referencias


Foto de Towfiqu barbhuiya en Unsplash.