GHSA-cg2g-xh64-v4q7:シャドウルート内のDOMPurify SAFE_FOR_TEMPLATESバイパス
DOMPurify 3.4.4~3.4.16では、アタッチされたシャドウルート内で分割された${...}式がSAFE_FOR_TEMPLATESを通過します(クリティカル、CVSS v4.0 9.2)。3.4.17にアップグレードしてください。
要点
- npmで週に約4,500万回ダウンロードされるHTMLサニタイザーDOMPurifyは、アタッチされたシャドウルート内でテキストノード間に分割され、サニタイズ後に再結合するテンプレート式を除去できません。この欠陥には、
SAFE_FOR_TEMPLATES: trueとDOMノードモード(RETURN_DOM、RETURN_DOM_FRAGMENT、IN_PLACE)の併用が必要です。 - シャドウルートを持つDOMノードを入力できる攻撃者は、
${...}などの式を、禁止要素で隔てられたテキストノードに分割します。DOMPurifyが区切りを削除すると断片が隣接し、再構成された式が出力に残ります。後段のテンプレートエンジンが評価すると、クロスサイトスクリプティング(XSS)、またはサーバー上のテンプレートインジェクションになります。 - 3.4.4~3.4.16が影響を受けます。アドバイザリの評価はクリティカル、CVSS v4.0 9.2です。概念実証(PoC)は公開されていますが、2026年10月23日時点で実環境の悪用は確認されておらず、CISA KEVカタログにも登録されていません。
- DOMPurify 3.4.17にアップグレードしてください。できない場合は、信頼できないマークアップを、解析済みDOMノードではなく文字列として渡してください。文字列入力ではDOMPurify自身のパーサーが宣言的シャドウルートを無視します。
影響を受けるか
次の条件をすべて満たす場合に影響を受けます。
- 依存関係がDOMPurify 3.4.4~3.4.16に解決される
- 設定で
SAFE_FOR_TEMPLATESを有効にしている(出力をテンプレートエンジンに渡せるよう、{{...}}、${...}、<%...%>の構文を除去するオプション) - 文字列を返す代わりにDOMノードを扱うモード、すなわち
RETURN_DOM、RETURN_DOM_FRAGMENT、IN_PLACE(入力ノードをその場でサニタイズ)でsanitizeを呼ぶ。本記事ではこれらをDOMノードモードと呼ぶ - 入力が、すでにアタッチ済みのシャドウルートを持つDOMノードになり得る
信頼できない入力を文字列としてサニタイズするアプリケーションは、この経路を通りません。3.4.17のREADMEにあるとおり、DOMPurify自身のパーサーは宣言的シャドウルートを無視します。
| ブランチ | 影響を受けるバージョン | 修正バージョン |
|---|---|---|
| DOMPurify 3.4.x | 3.4.4 – 3.4.16 | 3.4.17 |
| DOMPurify ≤ 3.4.3 | 影響なし。この不具合が依存するシャドウルート走査と走査後の除去処理は、いずれも3.4.4で導入 | - |
インストール済みのバージョンと、危険なオプションの使用箇所を確認します。
# Installed version
npm ls dompurify
node -e "console.log(require('dompurify/package.json').version)"
# Lock file check (matches lockfileVersion 1 "dompurify" and v2/v3 "node_modules/dompurify" keys)
grep -nE '"(node_modules/)?dompurify"' package-lock.json
# Container image check
docker run --rm <your-image> node -e "console.log(require('dompurify/package.json').version)"
# Find the risky configuration combination in your code
# (-A 8 lets the second grep see sibling options on adjacent lines)
grep -rn -A 8 "SAFE_FOR_TEMPLATES" --include="*.js" --include="*.ts" . | grep -iE "RETURN_DOM|IN_PLACE"
入力範囲を広げる微妙な点があります。シャドウルートはattachShadowでプログラム的に作る必要はありません。宣言的シャドウDOMに対応するブラウザーでは、<template shadowrootmode="open">を含むマークアップをsetHTMLUnsafe()やDocument.parseHTMLUnsafe()で解析すると、同じアタッチ済みシャドウルートができます。そのため、単なるHTML文字列でも、アプリがサニタイズ前にその方法で解析すれば欠陥に到達します。
修正方法
DOMPurifyをアップグレードします。
npm install dompurify@3.4.17
# or: pnpm add dompurify@3.4.17 | yarn add dompurify@3.4.17
すぐにアップグレードできない場合、バージョンではなく呼び出し方を変えます。
- 信頼できないマークアップを文字列として
sanitizeに渡す。文字列入力ではDOMPurifyのパーサーが宣言的シャドウルートを無視するため、走査が見落とすアタッチ済みシャドウルートは存在しません。3.4.17のREADMEに記載された動作です。 - DOMノードを受け取る必要がある場合、アップグレードするまで
SAFE_FOR_TEMPLATESをIN_PLACE、RETURN_DOM、RETURN_DOM_FRAGMENTと併用しない。フラグメントもアタッチ済みシャドウルートを持てるため、他の2つのモードと同じ経路を通ります。
この回避策はコードベース全体の呼び出し箇所で規律を守ることに依存します。一方、修正はすべての呼び出し元に対してサニタイザー内部の欠陥を取り除きます。
根本原因
SAFE_FOR_TEMPLATESは2つの仕組みでテンプレート式を無効化します。不具合は、2つ目の仕組みがシャドウルートを見られないことです。
最初の仕組みは主走査中のテキストノードごとの除去です。_sanitizeElementsが各ノードを訪れる際(影響リリースのsrc/purify.ts:2246)、_stripTemplateExpressionsが正規表現に一致するものを除去します。分割された式では、各断片が単独では区切りのパターンに一致しないため、これを回避します。MUSTACHE_EXPRには{{または}}、TMPLIT_EXPRには${、ERB_EXPRには<%または%>が必要です。単独の$はただのテキストです。
2つ目の仕組みは、まさにそのケースへの安全策です。走査後、_scrubTemplateExpressions(src/purify.ts:1717)がツリーのnormalize()を呼び、隣接するテキストノードを結合して、結合後に初めて形成された式を再度除去します。分割された式を検出できるのは、この処理だけです。アドバイザリは、シャドウ内の内容に届かない理由を説明しています。
_scrubTemplateExpressions (src/purify.ts:1717-1751):
- 1718行目のnode.normalize()はシャドウルート内のテキストノードを
結合しない(DOM仕様)。
- 1723-1731行目のNodeIteratorはシャドウツリーへ降りない
(DOM仕様)。
- 1743-1750行目の明示的な再帰はquerySelectorAll('template')によって
<template>.contentだけに入る。アタッチ済みシャドウルートに入る
同等の再帰は存在しない。
シャドウルート自体はサニタイズされますが、それは先行する_sanitizeShadowDOM処理(src/purify.ts:2710)だけです。この処理はノード単位の除去と禁止要素の削除を行いますが、走査後の正規化と再除去は行いません。つまり、シャドウルート内のテキストはノードごとにサニタイズされ、結合された単位として再確認されません。
不変条件:要素削除後に隣接テキストノードを結合し、式を再度除去すること。主ツリーと<template>の内容では成立しています。しかし、アタッチ済みシャドウルート内では何もこの条件を保証しないため、サニタイズ後に初めて断片が隣接する分割式が残ります。
これは以前の修正の回帰ではなく、別の境界の問題です。同シリーズの先行アドバイザリCVE-2026-65900(GHSA-gvmj-g25r-r7wr)は、走査後の処理を<template>.contentへの再帰に拡張しましたが、回帰テストはテンプレート内容だけを対象にしていました。その拡張にシャドウルートは含まれていませんでした。
悪用の仕組み
攻撃者は、禁止要素で隔てられたテキストノードに式を分割したシャドウルートを持つホスト要素を作ります。アプリケーションがSAFE_FOR_TEMPLATESでその場でサニタイズします。
// Proof of concept from GHSA-cg2g-xh64-v4q7 (Cure53 advisory), trimmed
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 });
host.shadowRoot.normalize();
console.log(host.shadowRoot.textContent);
// "${alert(document.domain)}"
攻撃経路を順に説明します。
- アプリケーションが
SAFE_FOR_TEMPLATES: trueとDOMノードモード(IN_PLACE、RETURN_DOM、RETURN_DOM_FRAGMENT)でDOMPurify.sanitize(node, cfg)を呼び、アタッチ済みシャドウルートを持つDOMノードを渡します。 - 攻撃者はシャドウルートのテキストノードと、その間の禁止された区切り要素を制御します。
$と{alert(document.domain)}の各断片は、単独では無害なテキストです。 _sanitizeAttachedShadowRootsがシャドウルートを_sanitizeShadowDOMに渡します。この処理がテキストノードごとに式を除去し、禁止された区切りを削除すると、断片が隣接します。- 結合された式を検出できる唯一の処理である走査後の安全策は、シャドウルートに入りません。
- 隣接する断片が
shadowRoot.textContentやshadowRoot.innerHTMLで一つの文字列としてシリアライズされ、完全な式を再構成します。 - 後段のテンプレート評価処理がシャドウルートの内容を読み、攻撃者の式を評価します。エンジンのコンテキストでのスクリプト実行、またはシンクがサーバー側のエンジンならテンプレートインジェクションになります。
この悪用には、DOMPurifyの返した内容を評価するシンクが必要です。アドバイザリはインジェクションの基本操作を実証し、典型的な評価処理としてnew Function('return `' + content + '`')()や、${...}、{{...}}を扱うエンジンを挙げています。機密性や完全性への影響は、後段の評価コンテキストに依存します。
分割された式がテンプレートエンジンに届く流れ
結合と再除去は主ツリーとテンプレート内容だけで実行され、アタッチ済みシャドウルートへの対応がないため、分割された式が残ります。攻撃者の入力から評価までの流れです。
DOMノードを入力"] --> B["アプリがDOMノードモードと
SAFE_FOR_TEMPLATESでサニタイズ"] B --> C["ノード単位の除去が
分割された式の断片を見逃す"] C --> D["禁止された区切り要素が削除され
断片が隣接"] D --> E["走査後の除去は主ツリーと
テンプレート内容だけで実行"] E --> F["shadowRoot.textContentが
完全な式をシリアライズ"] F --> G["後段のテンプレートエンジンが
攻撃者の式を評価"]
欠けているチェックは結合と再除去です。シャドウルートへ入らないため、結合した断片から式を再除去する処理がありません。
攻撃者に可能なこと
実証されているのは、サニタイズ後のシャドウルートの内容に、攻撃者の選んだ完全なテンプレート式を注入する能力です。クライアント側ではアプリケーションのオリジンでXSSが発生します。サニタイズした内容がサーバー側テンプレートに渡される場合はテンプレートインジェクションとなり、表現力の高いエンジンでは、しばしばサーバー上のリモートコード実行へ発展します。
アドバイザリが挙げる攻撃者に必要な条件は次のとおりです。
- アプリケーションが
SAFE_FOR_TEMPLATESとDOMノードモードを同時に有効にする - アプリケーションが、すでにアタッチ済みシャドウルートを持つDOMノードを渡す。対応エンジンでは、宣言的シャドウDOMと
setHTMLUnsafe()またはDocument.parseHTMLUnsafe()で、通常のHTML文字列から生成可能 - アプリケーションが、サニタイズしたシャドウルートの内容をテンプレート評価処理へ渡す
- サニタイザー自体では認証、権限、ユーザー操作は不要。CVSS v4.0ベクトルは
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。アプリケーション側の3条件がすべて揃う必要があるため、攻撃の複雑さは高、攻撃要件あり
条件が揃えば、攻撃者の式は評価エンジンの権限で動作します。そのため、ブラウザーのセッションデータの窃取から、許容範囲の広いサーバー側テンプレートを介したコマンド実行まで、影響はシンクに依存します。
修正が機能する理由
DOMPurify 3.4.17(プルリクエスト#1672、コミットa77a3e7)は、走査後の不変条件をシャドウルートにも拡張します。_sanitizeShadowDOMがシャドウルートの走査を終えると、主ツリーと同じ正規化と再除去を実行するようになりました。
// cure53/DOMPurify, dist/purify.cjs.js (release 3.4.17)
const _sanitizeShadowDOM = function (fragment) {
...
+ if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
};
以前の再帰的な_scrubTemplateExpressionsは2つに分かれました。_scrubOwnCharacterDataは再帰しない中核処理です。normalize()を呼び、ノード自身のテキスト、コメント、CDATA、処理命令ノードを走査して式を除去します。ラッパーは引き続き<template>.contentへ再帰します。_sanitizeShadowDOMはすでに入れ子のシャドウルートとテンプレート内容への再帰の入口なので、フラグメントごとに中核処理を実行すれば、祖先ごとに入れ子のフラグメントを再走査せず、すべてを一度ずつ処理できます。
各シャドウルートで結合と再除去が実行されるため、式を分割する手法は機能しなくなります。禁止された区切りが削除され、normalize()が隣接断片を結合すると、再除去処理が完全な${...}を検出し、主ツリーと同じように除去します。
修正は完全でしょうか。影響する呼び出し経路のopenなシャドウルートについては、完全です。同じリリースは、サニタイズ中にツリーから離れるノードをその場でより厳格に無効化するなど、周辺の仕組みも強化しています。
一つ残る境界は、修正するのではなくREADMEに明記されました。closedなシャドウルートはスクリプトから読めないため、DOMPurifyも内容を確認できず、サニタイズもできません。これはサニタイザーの欠陥ではなくプラットフォームの制約です。信頼できないマークアップは文字列として渡すというガイダンスが引き続き適用されます。
侵害されたかを確認する方法
このアドバイザリには侵害指標が公開されていません。公開ガイダンスがないため、次の一般的な兆候を確認してください。
- サニタイズ済みDOMノードの出力を、テンプレートエンジン、
eval形式の呼び出し、new Functionへ渡す箇所。そこでサニタイズ後の出力に式の形をした内容があれば、バイパスが発生したことを意味する - シャドウルート内かどうかを問わず、ユーザー提供マークアップに由来するテンプレート構文(
${、{{、<%)を含む保存済みコンテンツ。バイパスはそのような内容を残す - アプリが通常接続しないドメインを示すクライアント側のエラーログやCSPレポート。サニタイズ後に起動した外向きリクエストや式の評価を示唆する
評価が行われたエビデンスが見つかった場合、評価エンジンが到達できた範囲に応じて、関連するセッションやサーバーのコンテキストが露出したものとして扱ってください。
経緯
- 2026年10月12日:Cure53がアドバイザリGHSA-cg2g-xh64-v4q7を公開し、プルリクエスト#1672を含むDOMPurify 3.4.17をリリース。報告者zx(GitHub:@manus-pi)に謝辞を記載。GitHubのアドバイザリでは@manus-useにも謝辞。
- 2026年10月22日:GitHub Advisory Databaseでアドバイザリを公開・レビュー。
- 2026年10月23日:本記事を公開。実環境の悪用は確認されておらず、CISA KEVカタログにも未登録。
よくある質問
DOMPurifyのデフォルト設定も影響を受けますか?
いいえ。SAFE_FOR_TEMPLATESとDOMノードモードを併用し、アタッチ済みシャドウルートを持つDOMノードを入力する必要があります。デフォルトの文字列サニタイズでは、DOMPurifyのパーサーが宣言的シャドウルートを無視するため、影響する経路を通りません。
SAFE_FOR_TEMPLATESは何のためのものですか?
サニタイズしたマークアップをテンプレートエンジンに渡すアプリケーション用のオプションです。{{...}}、${...}、<%...%>というテンプレート式の構文を除去し、攻撃者がサニタイザーを通して式を持ち込み、後でエンジンに実行させることを防ぎます。このアドバイザリは、シャドウルート内ではその除去が不完全だったことを示しています。
テンプレート内容は修正済みなのに、シャドウルートが見落とされた理由は?
先行修正のCVE-2026-65900は<template>.contentへの再帰を追加しましたが、テストはそのコンテナーだけを対象としていました。アタッチ済みシャドウルートは、走査後の処理のNodeIteratorが入れない別のDOM境界です。専用の処理が必要で、3.4.17が追加しています。
この問題にCVEはありますか? 2026年10月23日時点で、アドバイザリGHSA-cg2g-xh64-v4q7にCVE識別子はありません。GHSA IDで追跡されています。
影響パッケージはどれほど使われていますか?
dompurifyパッケージは、npmで2026年10月16~22日の週に45,293,289回ダウンロードされました。同梱されたコピーやベンダー化したコピーはnpm lsに出ないため、下流のバンドルも確認してください。
アップグレード後、信頼できないノードにIN_PLACEを使い続けられますか? 3.4.17はその経路でシャドウルートを処理し、走査中にノードが移動・削除される場合のその場での処理も強化しています。残る注意点はプラットフォームの制約です。closedなシャドウルートはスクリプト側のサニタイザーには見えないため、可能な限り、信頼できないマークアップは文字列として渡してください。
アプリケーションの影響範囲を点検
OstorlabでWebアプリケーションのセキュリティ評価を開始してください。上記の影響バージョンと呼び出し箇所のチェックを使い、自社のDOMPurify構成がこの脆弱性に到達するかを確認します。
参考資料
- Cure53のアドバイザリGHSA-cg2g-xh64-v4q7:https://github.com/cure53/DOMPurify/security/advisories/GHSA-cg2g-xh64-v4q7 (2026年10月23日確認)
- GitHub Advisory Database項目:https://github.com/advisories/GHSA-cg2g-xh64-v4q7 (2026年10月23日確認)
- 修正コミットa77a3e7(リリース3.4.17):https://github.com/cure53/DOMPurify/commit/a77a3e78b59f72fa4d1c83f22e121ad9cc7d3575 (2026年10月23日確認)
- プルリクエスト#1672:https://github.com/cure53/DOMPurify/pull/1672 (2026年10月23日確認)
- DOMPurify 3.4.17リリース:https://github.com/cure53/DOMPurify/releases/tag/3.4.17 (2026年10月23日確認)
- 同シリーズの先行アドバイザリCVE-2026-65900(GHSA-gvmj-g25r-r7wr):https://github.com/advisories/GHSA-gvmj-g25r-r7wr (2026年10月23日確認)
dompurifyのnpmダウンロード数(2026年10月16~22日の週に45,293,289件):https://api.npmjs.org/downloads/point/last-week/dompurify (2026年10月24日確認)