DOMPurifyの`SAFE_FOR_TEMPLATES`の回避策:テンプレート式が、DOM出力モードにおいて、関連付けられたシャドウルートの内容内に残存する。

## 概要

DOMPurifyが`SAFE_FOR_TEMPLATES: true`と共に、DOMを返すモード(`RETURN_DOM: true`、`RETURN_DOM_FRAGMENT: true`、または`IN_PLACE: true`)で設定されている場合、`{{...}}`、`${...}`、`<%...%>`といったテンプレート式が、関連付けられたシャドウルートのテキストコンテンツ内でサニタイズ処理を通過してしまう。後処理の安全策である`_scrubTemplateExpressions`(`src/purify.ts:1717`)は、`<template>.content`内に再帰的に処理を行うように拡張されたが、関連付けられたシャドウルート内には再帰的に処理を行わない。攻撃者が、シャドウルートを持つDOMノード(`IN_PLACE`やシャドウに対応した`RETURN_DOM`のインターフェース)を供給できる場合、許可されていない要素によって区切られたテキストノードに分割された式を、それらの要素が削除された後に隣接するようにし、最終的に完全な式としてシリアライズされ、その後のテンプレートエンジンによって評価されてしまう。

## 根本原因

`SAFE_FOR_TEMPLATES`は、テンプレート式を2つのメカニズムで無効化する:メインの処理中の各テキストノードに対する除去処理(`_stripTemplateExpressions`、`_sanitizeElements`の`src/purify.ts:2246`で適用)、および最終的な後処理パス(`_scrubTemplateExpressions`、`src/purify.ts:1717`)による、隣接するテキストノードの正規化と、結合後にのみ形成される式の再除去。各ノードに対する除去処理は、個々のフラグメントに分割された式を検出できない。なぜなら、各フラグメントは区切り文字の正規表現に適合しないためである。後処理パスは、結合された形式を検出する唯一のメカニズムであるが、構造的にシャドウルートの内容に到達できない。

```text
_scrubTemplateExpressions (src/purify.ts:1717-1751):
- node.normalize() は1718行で、シャドウツリー内のテキストノードを結合しません(DOM仕様)。
- NodeIterator は1723-1731行で、シャドウツリー内を探索しません(DOM仕様)。
- 明示的な再帰処理は1743-1750行で、`<template>`要素の`.content`のみを処理します。`querySelectorAll('template')`を使用しており、アタッチされたシャドウツリーへの対応する再帰処理はありません。
シャドウツリーは、_sanitizeShadowDOM (src/purify.ts:2710)によって事前にサニタイズされます。この処理では、各ノードに対してフィルタリングを適用しますが、後続の正規化と再サニタイズは行われません。

したがって、シャドウツリー内のテキストノードは、各ノードごとにサニタイズされますが、結合された単位として再評価されることはありません。

## 影響

アタッチされたシャドウツリーを含むDOMノードの入力を制御できる攻撃者は、サニタイズを回避し、後でアプリケーションのテンプレートエンジンによって評価されるテンプレート式を注入することができます。クライアントサイドのコンテキストでは、クロスサイトスクリプティング(XSS)を引き起こし、サーバーサイドのテンプレートコンテキストでは、テンプレートインジェクションに発展する可能性があります。この脆弱性を悪用するには、デフォルト以外のアプリケーションの設定が必要です。アプリケーションは、`SAFE_FOR_TEMPLATES`を有効にし、DOMを返すモードを使用し、既にアタッチされたシャドウツリーを持つDOMノードを渡し、サニタイズされたシャドウツリーの内容をテンプレート評価器に渡す必要があります。シャドウツリーは、プログラム的に`attachShadow`を使用して構築する必要はありません。宣言的なシャドウDOMをサポートするエンジンでは、攻撃者が提供したマークアップに`<template shadowrootmode="open">`が含まれており、`setHTMLUnsafe`または`Document.parseHTMLUnsafe`によって解析されると、同じアタッチされたシャドウツリーが生成されます。したがって、単純なHTML文字列が、アプリケーションがこのように設定されている場合に、この脆弱性を突くことができます。実証された脆弱性は、サニタイズ後のシャドウツリーの内容に、攻撃者が選択した完全な式を注入することです。結果として実行されるスクリプトと、それに伴う機密性または完全性の問題は、下流のエンジンの評価コンテキストに依存します。

## 実証コード
```

前提条件:
- 影響を受けるソースから構築された DOMPurify (dist/purify.js または ESM/CJS バンドル)。
- シャドウ DOM をサポートするブラウザ (または DOM 実装)。
- アプリケーションが、サニタイズ処理を行う前に、アタッチされたシャドウルートを持つ DOM ノードを構築し、その後、シャドウルートのテキストコンテンツをテンプレートとして評価する。

ホスト要素を構築し、そのシャドウルートに、許可されていない要素で区切られたテキストノードに分割されたテンプレート式を格納し、`SAFE_FOR_TEMPLATES` オプションを指定してインプレースでサニタイズします。

```javascript
const host = document.createElement('div');
const root = host.attachShadow({ mode: 'open' });

// "${alert(document.domain)}" を分割し、単一のフラグメントが正規表現に一致しないようにする。
// <object> はデフォルトの ALLOWED_TAGS に含まれておらず、サニタイズ処理中に削除されるため、周囲のテキストノードが隣接する状態になる。
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);
```

```text
${alert(document.domain)}
```

下流のテンプレート評価器が `shadowRoot.textContent` を読み込み (例えば `new Function('return `' + content + '`')()` または `${...}`/`{{...}}` エンジン)、完全な式を評価する。

## 攻撃の連鎖

1. **脆弱性:** アプリケーションは、`DOMPurify.sanitize(node, cfg)` 関数(`src/purify.ts:2854行目)を、`SAFE_FOR_TEMPLATES: true` および `IN_PLACE: true`(または、シャドウDOMに対応した `RETURN_DOM`)という設定で呼び出します。この関数には、既にアタッチされたシャドウルートを持つDOMノードが渡されます。
2. **制御:** 攻撃者は、シャドウルート内のテキストノードの内容と、それらの間の許可されていない区切り要素を制御できます。各フラグメント(例:`$`, `{alert(document.domain)}`)は、個別に無害なテキストノードとして有効です。
3. **経路:** `_sanitizeAttachedShadowRoots` 関数(`src/purify.ts:2977行目)は、シャドウルートを `_sanitizeShadowDOM` 関数(`src/purify.ts:2710行目)に渡します。この関数は、各テキストノードに対して式を削除し(`src/purify.ts:2246行目)、許可されていない区切り要素を削除し、フラグメントを隣接させます。
4. **防御:** 式の安全性を確認する処理 `_scrubTemplateExpressions` 関数(`src/purify.ts:3131-3132行目)は、結合された式を検出する唯一の箇所ですが、シャドウルート内への再帰処理は行われません(`src/purify.ts:1717-1751行目では、`<template>.content` 内のみへの再帰処理が行われます)。
5. **根本原因:** 隣接するフラグメントは、`shadowRoot.textContent` または `shadowRoot.innerHTML` で単一の連結された文字列としてシリアライズされ、完全な式が再構成されます。
6. **結果:** 下流のテンプレートエンジンは、攻撃者が選択した式を評価し、その結果、エンジン内でスクリプトの実行(またはテンプレートインジェクション)が実現されます。

## バイパスの証拠

関連する保護機能は `SAFE_FOR_TEMPLATES` であり、特に、隣接するテキストノードが結合された後にのみ形成される式に対する防御を行う、処理後のパスです。この防御は、以前、CVE-2026-65900 (GHSA-gvmj-g25r-r7wr) の脆弱性に対処するために、`src/purify.ts` の 1743-1750 行で `<template>.content` に対して `querySelectorAll('template')` の再帰処理を追加することで拡張されました。最も重要な課題は、どのコードパスにおいても、シャドウツリー内で同様の結合と再処理が行われるかどうかです。`_scrubTemplateExpressions` ( `src/purify.ts` の 1717 行、1747 行、3132 行、3141 行) のすべての呼び出し元を調査した結果、シャドウツリーが渡されるケースは存在せず、`_sanitizeShadowDOM` ( `src/purify.ts` の 2710 行) は `_scrubTemplateExpressions` や `normalize()` を呼び出していません。ノードごとのフィルタリングでは対応できません。`MUSTACHE_EXPR` ( `src/regexp.ts` の 3 行) は `{{` または `}}` を必要とし、`TMPLIT_EXPR` ( `src/regexp.ts` の 5 行) は `${` を必要とし、`ERB_EXPR` ( `src/regexp.ts` の 4 行) は `<%` または `%>` を必要としますが、これらはすべて単一の文字のフラグメントには存在しません。CVE-2026-65900 の回帰テストは `<template>.content` のみで実施されており、`SAFE_FOR_TEMPLATES` の下でのシャドウツリーの内容に関するテストは含まれていません。したがって、これは、提供されたテンプレートコンテンツの修正を回避するのではなく、別の修正境界 (シャドウツリー) における未修正のケースです。

## 影響を受けるバージョン

- **エコシステム:** `npm`
- **パッケージ:** `dompurify`
- **確認された影響範囲:** `>= 3.4.4, <= 3.4.16`
- **確認された最新リリース:** `3.4.16` ([npm registry](https://www.npmjs.com/package/dompurify/v/3.4.16))
- **修正状況:** 未修正

下限の 3.4.4 は、アタッチされたシャドウツリーのサニタイズ処理と `_scrubTemplateExpressions` の安全策の両方を導入したリリースです。3.4.4 から 3.4.7 までのリリースは、コンテンツコンテナの再帰処理なしで安全策を含んでおり、3.4.8 以降は `<template>.content` の再帰処理を追加していますが、シャドウツリーの再帰処理は依然として省略されています。公開されているリリースや現在の開発バージョンには、シャドウツリーの修正は含まれていません。

## 推奨される修正

`<template>.content`に適用されている、ウォーク処理後の正規化と再クリーンアップのルールを、添付されたシャドウルートにも適用します。シャドウルートのウォーク処理が`_sanitizeShadowDOM`によって完了した後、そのシャドウルートに対して`_scrubTemplateExpressions`を実行し、要素の削除によって生成された隣接するテキストノードを結合し、再度クリーンアップします。あるいは、`_scrubTemplateExpressions`が、テンプレートコンテンツと同様に、添付されたシャドウルートを検出し、再帰的に処理するようにします。この修正は、ネストされたシャドウルートや、`<template>.content`内のシャドウルートを対象とし、また、すべての式の種類に対応する必要があります。なぜなら、禁止されている要素が、隣接するフラグメントを生成する区切り文字として機能する可能性があるからです。

---

報告者:zx (GitHub: @manus-pi)。

影響を受けるパッケージ:
- npm (JavaScript) dompurify: 影響範囲は、バージョン3.4.4以上、3.4.16以下。3.4.17で修正済み。