Evasión de SAFE_FOR_TEMPLATES en DOMPurify: las expresiones de plantilla divididas sobreviven en el contenido de shadow roots adjuntos en modos de salida DOM
Resumen
Cuando DOMPurify se configura con SAFE_FOR_TEMPLATES: true junto con un modo que devuelve DOM (RETURN_DOM: true, RETURN_DOM_FRAGMENT: true o IN_PLACE: true), expresiones de plantilla como {{...}}, ${...} y <%...%> sobreviven a la sanitización dentro del contenido de texto de un shadow root adjunto. La red de seguridad posterior al recorrido _scrubTemplateExpressions (src/purify.ts:1717) se amplió para aplicar recursión en <template>.content, pero nunca recurre en shadow roots adjuntos. Un atacante que pueda suministrar un nodo DOM que contenga un shadow root (las superficies IN_PLACE y RETURN_DOM compatibles con shadow) puede dividir una expresión entre nodos de texto separados por elementos no permitidos; una vez eliminados dichos elementos, los fragmentos quedan adyacentes y se serializan en una expresión completa que un motor de plantillas posterior evaluará.
Causa raíz
SAFE_FOR_TEMPLATES neutraliza las expresiones de plantilla mediante dos mecanismos: una eliminación por nodo de texto durante el recorrido principal (_stripTemplateExpressions, aplicado en _sanitizeElements en src/purify.ts:2246), y una pasada final posterior al recorrido (_scrubTemplateExpressions, src/purify.ts:1717) que normaliza los nodos de texto adyacentes y vuelve a eliminar expresiones que solo se forman tras la unión. La eliminación por nodo no puede detectar una expresión dividida en fragmentos individuales porque cada fragmento no coincide con las expresiones regulares de delimitadores. La pasada posterior al recorrido es el único mecanismo capaz de detectar la forma unida, pero estructuralmente no puede acceder al contenido del shadow root.
_scrubTemplateExpressions (src/purify.ts:1717-1751):
- node.normalize() at line 1718 does not merge text nodes inside shadow roots (DOM spec).
- NodeIterator at lines 1723-1731 does not descend into shadow trees (DOM spec).
- Explicit recursion at lines 1743-1750 enters <template>.content only, via
querySelectorAll('template'); there is no equivalent recursion into attached
shadow roots.
Shadow roots are sanitized earlier by _sanitizeShadowDOM (src/purify.ts:2710), which
applies the per-node strip but never performs the post-walk normalize-and-rescrub.
Por lo tanto, los nodos de texto de los shadow roots solo se sanitizan por nodo y nunca se vuelven a examinar como una unidad unificada.
Impacto
Un atacante que controle una entrada de nodo DOM que contenga un shadow root adjunto puede inyectar expresiones de plantilla que sobrevivan a la sanitización y que posteriormente sean evaluadas por el motor de plantillas de la aplicación. En contextos de cliente, esto genera cross-site scripting; en un contexto de plantillas del lado del servidor, puede escalar a inyección de plantillas. La explotación requiere una configuración de aplicación no predeterminada: la aplicación debe habilitar SAFE_FOR_TEMPLATES y un modo que devuelva DOM, pasar un nodo DOM que ya contenga un shadow root adjunto y suministrar el contenido del shadow root sanitizado a un evaluador de plantillas. No es necesario construir el shadow root mediante programación con attachShadow: en motores compatibles con declarative shadow DOM, el marcado suministrado por el atacante que contenga <template shadowrootmode="open"> analizado mediante setHTMLUnsafe o Document.parseHTMLUnsafe produce el mismo shadow root adjunto, por lo que una cadena HTML simple alcanza la brecha una vez configurada la aplicación de esta forma. La capacidad demostrada es la inyección de una expresión completa elegida por el atacante en el contenido del shadow root posterior a la sanitización; la ejecución de código resultante y cualquier consecuencia en la confidencialidad o integridad dependen del contexto de evaluación del motor posterior.
Prueba de concepto
Preconditions:
- DOMPurify built from the affected source (dist/purify.js or an ESM/CJS bundle).
- A browser (or DOM implementation) with shadow DOM support.
- Application constructs a DOM node with an attached shadow root before sanitizing,
and later evaluates the shadow root's text content as a template.
Cree un elemento host cuyo shadow root contenga una expresión de plantilla dividida entre nodos de texto separados por elementos no permitidos, y luego sanitice in situ con SAFE_FOR_TEMPLATES:
const host = document.createElement('div');
const root = host.attachShadow({ mode: 'open' });
// Split "${alert(document.domain)}" so no single fragment matches a regex.
// <object> is not in the default ALLOWED_TAGS and is removed during sanitization,
// leaving the surrounding text-node siblings adjacent.
root.appendChild(document.createTextNode('$'));
root.appendChild(document.createElement('object'));
root.appendChild(document.createTextNode('{alert(document.domain)}'));
DOMPurify.sanitize(host, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true });
// After the disallowed element is removed, the fragments are adjacent and survive.
host.shadowRoot.normalize();
console.log(host.shadowRoot.textContent);
${alert(document.domain)}
Un evaluador de plantillas posterior que lea shadowRoot.textContent (por ejemplo, new Function('return ' + content + '')() o cualquier motor ${...}/{{...}}) evaluará a continuación la expresión completamente formada.
Cadena de ataque
- Exposición: La aplicación invoca
DOMPurify.sanitize(node, cfg)(src/purify.ts:2854) conSAFE_FOR_TEMPLATES: trueyIN_PLACE: true(o unRETURN_DOMcompatible con shadow), pasando un nodo DOM que ya posee un shadow root adjunto. - Control: El atacante controla los nodos de texto del shadow root y los elementos separadores no permitidos entre ellos; cada fragmento (
$,{alert(document.domain)}) es un nodo de texto válido e individualmente benigno. - Ruta:
_sanitizeAttachedShadowRoots(src/purify.ts:2977) envía el shadow root a_sanitizeShadowDOM(src/purify.ts:2710), que elimina expresiones por nodo de texto (src/purify.ts:2246) y suprime los elementos separadores no permitidos, dejando los fragmentos contiguos. - Protección: La red de seguridad posterior al recorrido
_scrubTemplateExpressions(src/purify.ts:3131-3132) es la única pasada que detectaría la expresión unida, pero nunca recurre en shadow roots (src/purify.ts:1717-1751solo recurre en<template>.content). - Primitiva: Los fragmentos adyacentes se serializan como una única cadena concatenada en
shadowRoot.textContent/shadowRoot.innerHTML, reconstituyendo la expresión completa. - Resultado: Un motor de plantillas posterior evalúa la expresión elegida por el atacante, logrando la ejecución de secuencias de comandos (o inyección de plantillas) en el contexto del motor.
Evidencia de evasión
La protección relevante es SAFE_FOR_TEMPLATES, en concreto la pasada posterior al recorrido que defiende contra expresiones que solo se forman tras la unión de nodos de texto adyacentes. Dicha defensa se amplió previamente a <template>.content bajo CVE-2026-65900 (GHSA-gvmj-g25r-r7wr) añadiendo recursión para querySelectorAll('template') en src/purify.ts:1743-1750. El mayor desafío consiste en determinar si alguna ruta de código realiza la operación equivalente de combinación y re-limpieza en shadow roots. Enumerar cada punto de llamada de _scrubTemplateExpressions (src/purify.ts:1717, 1747, 3132, 3141) muestra que ninguno recibe un shadow root, y _sanitizeShadowDOM (src/purify.ts:2710) nunca llama a _scrubTemplateExpressions ni a normalize(). La eliminación por nodo no puede compensar esta ausencia: MUSTACHE_EXPR (src/regexp.ts:3) requiere {{ o }}, TMPLIT_EXPR (src/regexp.ts:5) requiere ${, y ERB_EXPR (src/regexp.ts:4) requiere <% o %>, ninguno de los cuales aparece en un fragmento de un solo carácter. Las pruebas de regresión de CVE-2026-65900 evalúan únicamente <template>.content; ninguna prueba cubre el contenido de shadow roots bajo SAFE_FOR_TEMPLATES. Por consiguiente, la brecha es un caso no corregido en un límite de remediación distinto (shadow roots) y no una evasión de la corrección suministrada para contenido de plantillas.
Versiones afectadas
- Ecosistema:
npm - Paquete:
dompurify - Rango afectado confirmado:
>= 3.4.4, <= 3.4.16 - Última versión comprobada:
3.4.16(npm registry))) - Estado de corrección: no corregido
El límite inferior 3.4.4 corresponde a la versión que introdujo tanto la pasada de sanitización de shadow roots adjuntos como la red de seguridad _scrubTemplateExpressions; las versiones 3.4.4 a 3.4.7 incluyen la red de seguridad sin recursión en contenedores de contenido, y a partir de 3.4.8 se añadió la recursión en <template>.content, omitiendo todavía la recursión en shadow roots. Ninguna versión publicada ni revisión de desarrollo actual contiene una corrección para shadow roots.
Corrección sugerida
Hacer cumplir en los shadow roots adjuntos el mismo invariante posterior al recorrido de normalización y re-limpieza que ya se aplica a <template>.content. Una vez que _sanitizeShadowDOM finalice el recorrido de un shadow root, ejecute _scrubTemplateExpressions sobre dicho shadow root para que los nodos de texto adyacentes generados por la eliminación de elementos se unan y se vuelvan a limpiar; como alternativa, haga que _scrubTemplateExpressions detecte y recurra en shadow roots adjuntos al igual que lo hace con el contenido de plantillas. La solución debe abarcar shadow roots anidados y shadow roots dentro de <template>.content, y debe gestionar los tres dialectos de expresiones, dado que cualquier elemento no permitido puede actuar como separador para producir fragmentos contiguos.
Reportado por zx (GitHub: @manus-pi).
Qué hacer
La solución no está establecida.
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.4, < 3.4.17 | 3.4.17 |
Referencias
- Vector CVSS
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N- Debilidad (CWE)
CWE-79- Plataforma
- npm
- Paquete
dompurify- Publicada
- 22.10.2026
- Actualizado
- 23.10.2026 00:05
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-cg2g-xh64-v4q7 - 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
- 32
- Primera → última
- 08.2020 10.2026
- Sin corrección
- 1
- Explotada en ataques
- 0
Distribución por gravedad
- Crítica 3
- Alta 3
- Media 19
- Baja 7
Divulgaciones por año
- 2020: 3
- 2021: 0
- 2022: 0
- 2023: 1
- 2024: 3
- 2025: 1
- 2026: 24
-
Evasión de SAFE_FOR_TEMPLATES en DOMPurify: las expresiones de plantilla divididas sobreviven en el contenido de shadow roots adjuntos en modos de salida DOM 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.