Manipulación segura de DOM con la API de Sanitizer

Jack J
Jack J

Las aplicaciones tratan con cadenas no confiables todo el tiempo, pero renderizar ese contenido de forma segura como parte de un documento HTML puede ser complicado. Si no tienes el cuidado suficiente, puedes crear oportunidades de secuencias de comandos entre sitios (XSS) que los atacantes maliciosos pueden aprovechar.

Para mitigar ese riesgo, la nueva propuesta de la API de Sanitizer tiene como objetivo compilar un procesador sólido para que las cadenas arbitrarias se inserten de forma segura en una página.

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

Escapa la entrada del usuario

Cuando insertes la entrada del usuario, las cadenas de consulta, el contenido de las cookies y mucho más en el DOM, las cadenas deben escaparse correctamente. Se debe prestar especial atención a la manipulación del DOM con .innerHTML, en la que 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 de HTML en la cadena de entrada o la expandes con .textContent, no se ejecuta alert(0). Sin embargo, como <em> que agregó el usuario también se expande como una cadena, este método no se puede usar para mantener la decoración de texto en HTML.

Lo mejor que puedes hacer aquí no es escapar, sino sanitizar.

Sanitiza la entrada del usuario

El escape se refiere a reemplazar caracteres especiales de HTML por entidades HTML.

La sanitización se refiere a quitar 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 sanitizar correctamente, es necesario analizar la cadena de entrada como HTML, omitir las etiquetas y los atributos que se consideran dañinos y conservar los inofensivos.

El objetivo de la especificación propuesta de la API de Sanitizer es proporcionar ese 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.

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

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

Para obtener el resultado de la sanitización 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="">

Personaliza con la configuración

La API de Sanitizer está configurada de forma predeterminada para quitar las cadenas que activarían la ejecución de secuencias de comandos. Sin embargo, también puedes agregar tus propias personalizaciones al proceso de sanitización con 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 sanitización debe tratar el elemento especificado.

allowElements: Son los nombres de los elementos que el sanitizer debe conservar.

blockElements: Son los nombres de los elementos que el sanitizer debe quitar, mientras conserva sus elementos secundarios.

dropElements: Son los nombres de los elementos que el sanitizer 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 el sanitizer permitirá o rechazará los atributos especificados con las siguientes opciones:

  • allowAttributes
  • dropAttributes

allowAttributes y dropAttributes propiedades esperan listas de coincidencias de atributos, es decir, 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 se permiten, se seguirán aplicando otras configuraciones para 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 sanitización. La diferencia principal entre la API de Sanitizer y DOMPurify es que DOMPurify muestra el resultado de la sanitización como una cadena, que debes escribir en un elemento DOM con .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 resguardo cuando la API de Sanitizer no se implementa en el navegador.

La implementación de DOMPurify tiene algunas desventajas. Si se muestra 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 causadas por casos en los que el resultado del segundo análisis es diferente del primero.

HTML también necesita contexto para analizarse. Por ejemplo, <td> tiene sentido en <table>, pero no en <div>. Como DOMPurify.sanitize() solo toma una cadena como argumento, se tuvo que adivinar el contexto de análisis.

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

Estado de la API y compatibilidad con navegadores

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

Paso Estado
1. Crear explicación Completado
2. Crear borrador de especificación Completado
3. Recopilar comentarios y repetir el diseño Completado
4. Prueba de origen de Chrome Completado
5. Lanzamiento Intención de envío en M105

Mozilla: Considera que vale la pena crear un prototipo de esta propuesta worth prototyping y la está implementando de forma activa.

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

Cómo habilitar la API de Sanitizer

Browser Support

  • Chrome: 146.
  • Edge: 146.
  • Firefox: 148.
  • Safari: not supported.

Chrome está en proceso de implementar la API de Sanitizer. En Chrome 93 o versiones posteriores, puedes probar el comportamiento habilitando la marca about://flags/#enable-experimental-web-platform-features. En versiones anteriores de Chrome Canary y el canal para desarrolladores, puedes habilitarla con --enable-blink-features=SanitizerAPI. Consulta las instrucciones para ejecutar Chrome con marcas.

Firefox

Firefox también implementa la API de Sanitizer como una función experimental. Para habilitarla, 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 escucharlos. Comparte tus opiniones sobre los problemas de la API de Sanitizer en GitHub y analiza con los autores de la especificación y las personas interesadas en esta API.

Si encuentras errores o comportamientos inesperados en la implementación de Chrome, notifícalos. Selecciona los componentes Blink>SecurityFeature>SanitizerAPI y comparte detalles para ayudar a los implementadores a hacer 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