Krytyczna

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

  • Ujawniono: 22.10.2026
  • Aktualizacja: 23.10.2026 00:05

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:

js
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

  1. Ekspozycja: Aplikacja wywołuje DOMPurify.sanitize(node, cfg) (src/purify.ts:2854) z parametrami SAFE_FOR_TEMPLATES: true oraz IN_PLACE: true (lub wariantem RETURN_DOM obsługującym shadow), przekazując węzeł DOM, który posiada już dołączony shadow root.
  2. 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.
  3. Ś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.
  4. 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-1751 rekursuje wyłącznie do <template>.content).
  5. 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.
  6. 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.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Paczka npm dompurify >= 3.4.4, < 3.4.17 3.4.17

Odnośniki

Ocena CVSS -
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 00:05

Ź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ć.

Wszystkie (32) w archiwum
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

  1. 2020: 3
  2. 2021: 0
  3. 2022: 0
  4. 2023: 1
  5. 2024: 3
  6. 2025: 1
  7. 2026: 24
Istotność:
  1. 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
    Krytyczna ghsa-cg2g-xh64-v4q7 >= 3.4.4, < 3.4.17 3.4.17
  2. Wysoka · 8.2 CVE-2026-47423 >= 3.4.4, < 3.4.5 3.4.5
  3. Krytyczna · 9.1 CVE-2024-48910 < 2.4.2 2.4.2
  4. Wysoka · 10.0 CVE-2024-47875 < 2.5.0, >= 3.0.0, < 3.1.3 2.5.0, 3.1.3
  5. Wysoka · 7.0 CVE-2024-45801 < 2.5.4, >= 3.0.0, < 3.1.3 2.5.4, 3.1.3
  6. Krytyczna ghsa-mjjq-c88q-qhr6 < 2.0.7 2.0.7

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.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.