Obejście SAFE_FOR_TEMPLATES w DOMPurify: rozdzielone wyrażenia szablonów przetrwają w zawartości dołączonych shadow root w trybach wyjściowych DOM
Podsumowanie
Gdy biblioteka DOMPurify jest skonfigurowana z flagą SAFE_FOR_TEMPLATES: true wraz z trybem zwracającym DOM (RETURN_DOM: true, RETURN_DOM_FRAGMENT: true lub IN_PLACE: true), wyrażenia szablonów takie jak {{...}}, ${...} oraz <%...%> przetrwają sanityzację wewnątrz zawartości tekstowej dołączonego shadow root. Końcowy mechanizm ochronny _scrubTemplateExpressions (src/purify.ts:1717) został rozszerzony o rekursję do <template>.content, lecz nigdy nie rekursuje do dołączonych shadow root. Atakujący, który ma możliwość dostarczenia węzła DOM zawierającego shadow root (powierzchnie IN_PLACE oraz obsługujące shadow RETURN_DOM), może rozdzielić wyrażenie pomiędzy węzły tekstowe przedzielone niedozwolonymi elementami; po usunięciu tych elementów fragmenty stają się sąsiadujące i serializują się do pełnego wyrażenia, które zostanie przetworzone przez dalszy silnik szablonów.
Przyczyna źródłowa
SAFE_FOR_TEMPLATES neutralizuje wyrażenia szablonów za pomocą dwóch mechanizmów: oczyszczania pojedynczych węzłów tekstowych podczas głównego przejścia (_stripTemplateExpressions, stosowanego w _sanitizeElements w src/purify.ts:2246) oraz końcowego przejścia (_scrubTemplateExpressions, src/purify.ts:1717), które normalizuje sąsiadujące węzły tekstowe i ponownie usuwa wyrażenia powstające dopiero po ich połączeniu. Oczyszczanie pojedynczych węzłów nie jest w stanie wykryć wyrażenia rozdzielonego na poszczególne fragmenty, ponieważ każdy fragment z osobna nie pasuje do wyrażeń regularnych ograniczników. Przejście końcowe jest jedynym mechanizmem, który wychwyciłby połączoną formę, lecz ze względów strukturalnych nie dociera ono do zawartości 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.
Węzły tekstowe w shadow root są w związku z tym sanityzowane wyłącznie pojedynczo i nigdy nie są ponownie weryfikowane jako połączona całość.
Wpływ
Atakujący kontrolujący węzeł DOM zawierający dołączony shadow root może wstrzyknąć wyrażenia szablonów, które przetrwają sanityzację, a następnie zostaną zinterpretowane przez silnik szablonów aplikacji. W kontekstach po stronie klienta prowadzi to do cross-site scripting (XSS); w kontekście szablonów po stronie serwera może eskalować do wstrzyknięcia szablonu (template injection). Wykorzystanie podatności wymaga niestandardowej konfiguracji aplikacji: aplikacja musi włączyć SAFE_FOR_TEMPLATES oraz tryb zwracający DOM, przekazać węzeł DOM posiadający już dołączony shadow root i przekazać oczyszczoną zawartość shadow root do ewaluatora szablonów. Shadow root nie musi być tworzony programowo za pomocą attachShadow: w silnikach obsługujących deklaratywny shadow DOM znaczniki dostarczone przez atakującego zawierające <template shadowrootmode="open">, przetworzone przez setHTMLUnsafe lub Document.parseHTMLUnsafe, tworzą taki sam dołączony shadow root, więc zwykły ciąg HTML trafia w tę lukę, jeśli aplikacja jest tak skonfigurowana. Zademonstrowana możliwość polega na wstrzyknięciu kompletnego, wybranego przez atakującego wyrażenia do zawartości shadow root po sanityzacji; wynikające z tego wykonanie skryptu oraz wszelkie konsekwencje dla poufności lub integralności zależą od kontekstu ewaluacji w dalszym silniku.
Proof of Concept
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.
Należy utworzyć element hosta, którego shadow root zawiera wyrażenie szablonu rozdzielone pomiędzy węzły tekstowe odseparowane niedozwolonymi elementami, a następnie przeprowadzić sanityzację w miejscu za pomocą 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)}
Dalszy ewaluator szablonów odczytujący shadowRoot.textContent (na przykład new Function('return ' + content + '')() lub dowolny silnik ${...}/{{...}}) przetworzy wówczas w pełni uformowane wyrażenie.
Łańcuch ataku
- Ekspozycja: Aplikacja wywołuje
DOMPurify.sanitize(node, cfg)(src/purify.ts:2854) z parametramiSAFE_FOR_TEMPLATES: trueorazIN_PLACE: true(lub wariantemRETURN_DOMobsługującym shadow), przekazując węzeł DOM, który posiada już dołączony shadow root. - Kontrola: Atakujący kontroluje węzły tekstowe w shadow root oraz niedozwolone elementy rozdzielające między nimi; każdy fragment (
$,{alert(document.domain)}) jest prawidłowym, samodzielnie nieszkodliwym węzłem tekstowym. - Ścieżka:
_sanitizeAttachedShadowRoots(src/purify.ts:2977) przekazuje shadow root do_sanitizeShadowDOM(src/purify.ts:2710), który oczyszcza wyrażenia w pojedynczych węzłach tekstowych (src/purify.ts:2246) i usuwa niedozwolone elementy separujące, pozostawiając fragmenty bezpośrednio obok siebie. - Zabezpieczenie: Końcowy mechanizm ochronny
_scrubTemplateExpressions(src/purify.ts:3131-3132) jest jedynym przejściem, które wychwyciłoby połączone wyrażenie, lecz nigdy nie wchodzi rekursywnie do shadow root (src/purify.ts:1717-1751rekursuje wyłącznie do<template>.content). - Prymityw: Sąsiadujące fragmenty serializują się jako pojedynczy, połączony ciąg znaków we właściwościach
shadowRoot.textContent/shadowRoot.innerHTML, odtwarzając pełne wyrażenie. - Rezultat: Dalszy silnik szablonów ewaluuje wybrane przez atakującego wyrażenie, uzyskując wykonanie skryptu (lub wstrzyknięcie szablonu) w kontekście silnika.
Dowód obejścia zabezpieczeń
Istotnym zabezpieczeniem jest SAFE_FOR_TEMPLATES, a w szczególności przejście końcowe chroniące przed wyrażeniami powstającymi dopiero po połączeniu sąsiadujących węzłów tekstowych. Ochrona ta została wcześniej rozszerzona na <template>.content w ramach CVE-2026-65900 (GHSA-gvmj-g25r-r7wr) poprzez dodanie rekursji querySelectorAll('template') w src/purify.ts:1743-1750. Kluczową kwestią jest to, czy jakakolwiek ścieżka kodu realizuje analogiczne łączenie i ponowne oczyszczanie wewnątrz shadow root. Przegląd każdego miejsca wywołania _scrubTemplateExpressions (src/purify.ts:1717, 1747, 3132, 3141) wskazuje, że żadne z nich nie otrzymuje shadow root, a _sanitizeShadowDOM (src/purify.ts:2710) nigdy nie wywołuje _scrubTemplateExpressions ani normalize(). Oczyszczanie poszczególnych węzłów nie jest w stanie tego zrekompensować: MUSTACHE_EXPR (src/regexp.ts:3) wymaga {{ lub }}, TMPLIT_EXPR (src/regexp.ts:5) wymaga ${, a ERB_EXPR (src/regexp.ts:4) wymaga <% lub %> – żaden z tych elementów nie występuje w pojedynczym jednoliterowym fragmencie. Testy regresyjne CVE-2026-65900 weryfikują wyłącznie <template>.content; żaden test nie obejmuje zawartości shadow root przy włączonym SAFE_FOR_TEMPLATES. Luka stanowi zatem nienaprawiony przypadek na odrębnej granicy remediacyjnej (shadow root), a nie obejście wdrożonej poprawki dla zawartości szablonów.
Wersje dotknięte podatnością
- Ekosystem:
npm - Pakiet:
dompurify - Potwierdzony zakres podatności:
>= 3.4.4, <= 3.4.16 - Ostatnie sprawdzone wydanie:
3.4.16(npm registry))) - Status naprawy: nienaprawione
Dolna granica 3.4.4 to wersja, w której wprowadzono zarówno przebieg sanityzacji dołączonych shadow root, jak i mechanizm ochronny _scrubTemplateExpressions; wydania od 3.4.4 do 3.4.7 zawierały ten mechanizm bez rekursji do kontenerów zawartości, a od wersji 3.4.8 dodano rekursję do <template>.content, nadal jednak pomijając rekursję do shadow root. Żadne opublikowane wydanie ani bieżąca wersja rozwojowa nie zawiera poprawki dla shadow root.
Sugerowane rozwiązanie
Wdrożenie dla dołączonych shadow root takiego samego niezmiennika normalizacji i ponownego oczyszczania po zakończeniu przejścia, jaki obowiązuje już dla <template>.content. Po zakończeniu przetwarzania shadow root przez _sanitizeShadowDOM należy uruchomić _scrubTemplateExpressions na tym shadow root, aby sąsiadujące węzły tekstowe powstałe wskutek usunięcia elementów zostały połączone i ponownie oczyszczone; alternatywnie można zmodyfikować _scrubTemplateExpressions, aby wykrywał dołączone shadow root i rekursywnie je przetwarzał w taki sam sposób, jak zawartość szablonów. Poprawka musi obejmować zagnieżdżone shadow root oraz shadow root wewnątrz <template>.content, a także obsługiwać wszystkie trzy dialekty wyrażeń, ponieważ dowolny niedozwolony element może posłużyć jako separator generujący sąsiadujące fragmenty.
Zgłoszone przez zx (GitHub: @manus-pi).
Co trzeba zrobić
Środek zaradczy nie został ustalony.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | dompurify | >= 3.4.4, < 3.4.17 | 3.4.17 |
Odnośniki
- Wektor 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- Słabość (CWE)
CWE-79- Platforma
- npm
- Paczka
dompurify- Ujawniono
- 22.10.2026
- Aktualizacja
- 23.10.2026 10:00
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-cg2g-xh64-v4q7 - CC BY 4.0
Historia podatności: dompurify
Wszystkie opisane podatności tej paczki (Paczka npm), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.
- Wszystkich wpisów
- 32
- Pierwsza → ostatnia
- 08.2020 10.2026
- Bez poprawki
- 1
- Wykorzystywana w atakach
- 0
Rozkład istotności
- Krytyczna 3
- Wysoka 3
- Średnia 19
- Niska 7
Ujawnienia w latach
- 2020: 3
- 2021: 0
- 2022: 0
- 2023: 1
- 2024: 3
- 2025: 1
- 2026: 24
-
Obejście SAFE_FOR_TEMPLATES w DOMPurify: rozdzielone wyrażenia szablonów przetrwają w zawartości dołączonych shadow root w trybach wyjściowych DOM ten wpis
Dowiedz się, które podatności dotyczą Twojej aplikacji
Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.