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.
Las aplicaciones trabajan 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 se tiene el cuidado suficiente, es fácil crear accidentalmente oportunidades para el scripting 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. En este artículo, se presenta la API y se explica cómo usarla.
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
Cómo escapar la entrada del usuario
Cuando insertes en el DOM entradas del usuario, cadenas de consulta, contenido de cookies, etcétera, las cadenas deben escaparse correctamente. Se debe prestar especial atención a la manipulación del DOM a través de .innerHTML, en la que las cadenas sin escape son una fuente típica de XSS.
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
$div.innerHTML = user_input
Si escapas los caracteres especiales de HTML en la cadena de entrada anterior o la expandes con .textContent, no se ejecutará alert(0). Sin embargo, dado que <em> agregado por el usuario también se expande como una cadena tal como está, 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.
Limpieza de la entrada del usuario
Diferencia entre escapar y limpiar
El escape se refiere al reemplazo de caracteres HTML especiales por entidades HTML.
La sanitización se refiere a la eliminación de 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="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
Para limpiar correctamente, es necesario analizar la cadena de entrada como HTML, omitir las etiquetas y los atributos que se consideran dañinos, y conservar los que no lo son.
La especificación propuesta de la API de Sanitizer tiene como objetivo proporcionar dicho procesamiento como una API estándar para los navegadores.
API de Sanitizer
La API de Sanitizer se usa de la siguiente manera:
const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
Sin embargo, { sanitizer: new Sanitizer() } es el argumento predeterminado. Por lo tanto, puede ser como se muestra a continuación.
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
Cabe destacar que setHTML() se define en Element. Como es un método de Element, el contexto para analizar se explica por sí mismo (<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 limpieza como una cadena, puedes usar .innerHTML de los resultados de setHTML().
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g src=""
Personalización a través de la configuración
La API de Sanitizer está configurada de forma predeterminada para quitar las cadenas que activarían la ejecución de la secuencia de comandos. Sin embargo, también puedes agregar tus propias personalizaciones al proceso de saneamiento 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 sanitización debe tratar el elemento especificado.
allowElements: Nombres de los elementos que debe conservar el filtro.
blockElements: Nombres de los elementos que el filtro debería quitar, pero conservando sus elementos secundarios.
dropElements: Nombres de los elementos que el filtro 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
También puedes controlar si el sanitizador permitirá o rechazará atributos específicos con las siguientes opciones:
allowAttributesdropAttributes
Las propiedades allowAttributes y dropAttributes esperan listas de coincidencias de atributos, es decir, objetos cuyas claves son nombres de atributos y cuyos valores son listas de elementos objetivo o el comodín *.
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/div
allowCustomElements es la opción para permitir o rechazar elementos personalizados. Si se permiten, se seguirán 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 >})
//< divcustom-e><lemh>ello/custom-elem/div
Superficie de la API
Comparación con DomPurify
DOMPurify es una biblioteca conocida que ofrece funcionalidad de saneamiento. La principal diferencia entre la API de Sanitizer y DOMPurify es que DOMPurify devuelve el resultado de la sanitización como una cadena, que debes escribir en un elemento DOM a través de .innerHTML.
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o world/em>img src=""`
DOMPurify puede servir como alternativa cuando la API de Sanitizer no se implementa en el navegador.
La implementación de DOMPurify tiene algunas desventajas. Si se devuelve una cadena, DOMPurify y .innerHTML analizarán la cadena de entrada dos veces. Este doble análisis 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.
El HTML también necesita que se analice el contexto. Por ejemplo, <td> tiene sentido en <table>, pero no en <div>. Dado que 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 realizar un análisis doble y aclarar el contexto del análisis.
Estado de la API y compatibilidad con navegadores
La API de Sanitizer se encuentra en proceso de estandarización y Chrome está en proceso de implementarla.
| Paso | Estado |
|---|---|
| 1. Crea una explicación | Completar |
| 2. Crea un borrador de especificación | Completar |
| 3. Recopila comentarios y realiza iteraciones en el diseño | Completar |
| 4. Prueba de origen de Chrome | Completar |
| 5. Lanzamiento | Intención de lanzar en M105 |
Mozilla: Considera que esta propuesta vale la pena crear un prototipo 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
Habilitación a través de about://flags o la opción de CLI
Chrome
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 del 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 una función experimental. Para habilitarlo, establece la marca dom.security.sanitizer.enabled en true en about:config.
Detección de características
if (window.Sanitizer) {
// Sanitizer API is enabled
}
Comentarios
Si pruebas esta API y tienes comentarios, nos encantaría conocerlos. Comparte tus opiniones sobre los problemas de GitHub de la API de Sanitizer y debate 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, informa el error. Selecciona los componentes de 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 Sanitizer API Playground de Mike West:
Referencias
- Especificación de la API de HTML Sanitizer
- Repositorio de WICG/sanitizer-api
- Preguntas frecuentes sobre la API de Sanitizer
- Documentación de referencia de la API de HTML Sanitizer en MDN
Foto de Towfiqu barbhuiya en Unsplash.