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:
allowAttributesdropAttributes
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 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: