# Next.js / React のXSS・DOM-XSS対策 — dangerouslySetInnerHTML の穴と、安全なサニタイズ・CSP

> Reactは標準でJSXをエスケープするが、dangerouslySetInnerHTML・href=javascript:・ref経由のDOM操作・DOMシンクで抜ける。XSS/DOM-XSSの発生箇所、DOMPurifyによる出力時サニタイズ、CSP(nonce)/Trusted Typesの多層防御を、脆弱→修正の実コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Next.js, React, セキュリティ, TypeScript
- URL: https://tomodahinata.com/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide
- カテゴリ: アプリ層セキュリティ
- 総合ガイド: https://tomodahinata.com/blog/nextjs-supabase-application-security-guide

## 要点

- ReactはJSXの式展開を自動でHTMLエスケープするためXSSに強い。だが穴は『設計者が自分で開ける』——dangerouslySetInnerHTML、href/srcのjavascript:スキーム、ref経由のinnerHTML、サードパーティHTMLの4経路
- DOM-XSSはサーバーを介さずブラウザ内で完結する。location.hash等の汚染source→innerHTML/eval/document.write等の危険sinkへ、検証なしに値が流れたとき発生し、サーバーログにもWAFにも映りにくい
- 信頼できないHTMLは『出力時にDOMPurifyでサニタイズ』が基本。最も堅いのは『そもそも生HTMLを文字列で持たない』設計。URLは正規表現でなくURL APIでスキームを検証する
- Trusted TypesとCSP(nonce)は多層防御——XSSの混入を防ぐのではなく、混入しても実行・悪用させない『最後の壁』。CSPは緩い設定だと油断を生むだけで防御にならない
- サニタイズ忘れや汚染→シンクの流れは静的解析で機械的に検出できる。だが『そのHTMLは本当に信頼できるか』『サニタイザのallowlistは文脈に合っているか』という安全な出力設計はコードレビューでしか守れない

---

最初に結論を述べます。**Reactは、JSXに埋め込んだ値を標準でHTMLエスケープするため、素直に書いている限りXSSに強い。問題は、その防御を無効化する「抜け道」がフレームワーク自身に用意されていて、しかもそれを開けるのは攻撃者ではなく開発者だ、という点です。** 代表的な穴は4つ——`dangerouslySetInnerHTML`、`href` / `src` の `javascript:` スキーム、`ref` 経由の直接DOM操作、そしてサードパーティHTMLの埋め込みです。

これは「Reactは危ない」という話ではありません。むしろ逆で、**Reactのデフォルトは安全側に倒れています。** 危ないのは、その安全弁を「手元で動かしたいから」と外す瞬間です。本記事は、XSS（反射型・格納型・DOM-XSS）がNext.js / Reactのどこに空くのかを地図にし、万一 `<script>` を仕込まれても被害を小さくするための出力時サニタイズ・URLスキーム検証・Trusted Types・CSP(nonce)を、脆弱→修正の実コードで体系化します。XSSはOWASP Top 10のインジェクション（A03）に含まれる最頻出クラスの1つです（[OWASP Top 10](https://owasp.org/www-project-top-ten/)）。アプリ全体のセキュリティ地図は[Next.js × Supabase アプリケーションセキュリティ完全ガイド](/blog/nextjs-supabase-application-security-guide)にまとめており、本記事はそのうち「XSS／出力の安全性」に絞った深掘りです。

---

## 1. 前提：ReactはデフォルトでXSSをエスケープする

最初に、Reactの「効いている防御」を正確に理解します。JSXの `{}` に渡した値は、**テキストノード**として描画されます。HTMLとしては解釈されないため、文字列に含まれるタグはそのまま画面に表示されるだけです。

```tsx
// React は {} の中身を「テキスト」として描画する＝HTMLとして解釈しない
function Comment({ body }: { body: string }) {
  // body が "<script>alert(1)</script>" でも、画面には文字列として出るだけ
  return <p>{body}</p>;
}
```

属性値も同様にエスケープされます。だから「ユーザー入力をそのまま画面に出す」だけならXSSは起きません。**にもかかわらずXSSが起きるのは、開発者がこのデフォルトを意図的に外すときだけ**です。その出口を一覧にします。

| 抜け道 | 何が起きるか | 主なXSSクラス |
|---|---|---|
| `dangerouslySetInnerHTML` | 文字列を**HTMLとして解釈**して挿入する | 格納型 / 反射型 |
| `href` / `src` に `javascript:` | クリック・読み込みでスクリプトが走りうる | 反射型 / DOM |
| `ref` 経由の `el.innerHTML = ...` | Reactの外で**DOMを直接操作**する | DOM |
| サードパーティHTMLの埋め込み | CMS・広告・メール本文を「信頼」してしまう | 格納型 |

共通するのは、いずれも「**文字列を、HTMLやコードとして実行される文脈に流し込む**」点です。逆に言えば、ここさえ押さえれば守りの大半は決まります。

---

## 2. XSSの三分類とNext.jsでの現れ方

XSSは「攻撃コードがどこを経由するか」で3つに分かれます。Next.jsでは、サーバー（RSC / SSR）とクライアント（ハイドレーション後）のどちらで起きるかが対策に直結します。

| 種類 | 経路 | Next.jsでの典型 |
|---|---|---|
| **反射型（Reflected）** | リクエスト値が即座に応答へ反射 | `searchParams` を `dangerouslySetInnerHTML` でSSR描画 |
| **格納型（Stored）** | DB等に保存された汚染HTMLが後で描画 | 投稿本文・プロフィールHTMLを保存→表示 |
| **DOM-XSS** | source→sinkがすべてブラウザ内で完結 | `location.hash` を `innerHTML` に書き戻す |

ここで重要なのが**DOM-XSS**です。反射型・格納型はサーバーを通るため、サーバーログやWAFに痕跡が残ることがあります。一方DOM-XSSは、攻撃の起点がURLフラグメント（`#...`）のように**サーバーへ送信されない値**であることが多く、サーバーからは一切見えません。Reactのデフォルトエスケープも、`innerHTML` のようなブラウザの生DOM APIを直接叩く以上、効きません。**「サーバー側のサニタイズで全部守る」という発想がDOM-XSSに通用しない**のはこのためです。

---

## 3. 抜け道①：dangerouslySetInnerHTML

名前が警告そのものです。これは「innerHTML を React 経由で設定する」APIで、渡した文字列は**HTMLとして解釈**されます。問題は、その文字列の出所が信頼できるかどうかを、APIは一切確かめないことです。

```tsx
// 脆弱：APIから受け取ったHTMLをそのまま挿入（格納型XSS）
function Article({ id }: { id: string }) {
  const [html, setHtml] = useState("");
  useEffect(() => {
    fetch(`/api/articles/${id}`)
      .then((r) => r.json())
      .then((d) => setHtml(d.bodyHtml)); // ← DB由来の汚染HTML
  }, [id]);

  // bodyHtml に <img src=x onerror=...> が混ざっていれば実行される
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
```

`<script>` タグは挿入後に実行されないブラウザ仕様がありますが、`onerror` などのイベントハンドラ属性は実行されます。つまり「スクリプトタグを弾けばいい」では塞げません。

判断基準はただ1つ——**「このHTMLは、攻撃者が1ビットも関与できない出所か？」** です。サーバーで自分が組み立てたJSON-LDのような完全に信頼できる出力なら許容されます（このサイトでも構造化データの埋め込みにのみ使っています）。しかし、リクエスト・DB・外部APIに由来する値が混ざるなら、**出力時に必ずサニタイズ**します（設定の詳細は第7節）。

```tsx
// 修正：出力の直前にサニタイズする（DOMPurifyの設定は第7節で詳述）
import DOMPurify from "isomorphic-dompurify";

function Article({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
```

---

## 4. 抜け道②：href / src の javascript: スキーム

JSXは属性値をエスケープしますが、**HTMLエスケープはURLスキームを無害化しません。** `javascript:` で始まるURLは、文法上は正しいURLだからです。動的に組み立てた値を `href` や `src` に渡すと、クリックや読み込みでスクリプトが走りうる経路になります。

```tsx
// 脆弱：ユーザー入力のURLをそのまま href に渡す
function ProfileLink({ website }: { website: string }) {
  // website が "javascript:/* 任意コード */" だと、クリックで実行されうる
  return <a href={website}>サイト</a>;
}
```

React 16.9以降、`javascript:` URLには開発時に警告が出ますが、**警告は防御ではありません。** 値を信頼するか否かは自分で決める必要があります。対策は「**許可するスキームのallowlist**」です。ここで正規表現を自作してはいけません。`java\tscript:` のようにタブや改行を挟む難読化があり、手書きの正規表現はすり抜けられます。標準の `URL` パーサはこうした制御文字を仕様に従って除去してから解釈するため、スキーム判定が堅牢になります。

```ts
// lib/safe-href.ts — 許可スキームだけ通す。URL APIで難読化に強く判定する
const SAFE_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:"]);

export function safeHref(raw: string): string {
  try {
    // 相対URLも解決できるよう base を渡す
    const protocol = new URL(raw, "https://example.com").protocol;
    return SAFE_PROTOCOLS.has(protocol) ? raw : "#";
  } catch {
    return "#"; // パースできない値は通さない
  }
}
```

```tsx
// 修正：スキームを検証してから渡す
import { safeHref } from "@/lib/safe-href";

function ProfileLink({ website }: { website: string }) {
  return <a href={safeHref(website)} rel="noopener noreferrer">サイト</a>;
}
```

なお、スキーム検証は `javascript:` や `data:` を止めますが、**リンクが「どこへ飛ぶか」（外部の悪性サイトへの誘導）は別問題**です。戻り先URLを信頼することの危うさは、地続きのオープンリダイレクトとして[Next.jsのオープンリダイレクト対策](/blog/nextjs-open-redirect-callback-url-prevention-guide)で扱っています。

---

## 5. 抜け道③：ref経由のDOM操作とサードパーティHTML

Reactの安全性は「Reactを通してDOMを更新する」前提で成り立ちます。`ref` で生のDOM要素を取り出し、`innerHTML` を直接代入すると、その前提を自分で破ることになります。

```tsx
// 脆弱：ref で取った要素に innerHTML を直接代入（Reactのエスケープを迂回）
function Banner({ markup }: { markup: string }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current) ref.current.innerHTML = markup; // 危険シンク
  }, [markup]);
  return <div ref={ref} />;
}
```

修正の原則は「**生DOM操作をやめ、Reactの描画に戻す**」ことです。表示したいのが**テキスト**なら `textContent` を使えばHTMLとして解釈されません。HTMLが必要なら、第7節のサニタイズを通します。

```tsx
// 修正：テキストなら state 経由で描画（自動エスケープに戻る）
function Banner({ text }: { text: string }) {
  return <div>{text}</div>;
}
```

**サードパーティHTML**も同じ穴です。CMSのリッチテキスト、広告タグ、メール本文のプレビュー——これらを「外部のものだから整形済みだろう」と信頼して `dangerouslySetInnerHTML` に流すのは危険です。複数ユーザーが編集できるCMSは、その時点で「信頼できる出所」ではありません。**「信頼できるか」は技術ではなく、誰がそのデータを書けるかという設計上の判断**であり、ここがツールに任せきれない領域です。

---

## 6. DOM-XSS：sourceからsinkへの流れを断つ

DOM-XSSは、クライアント側で「**攻撃者が操作できる値（source）が、検証されないまま危険な処理（sink）に届く**」ときに起きます。第2節のとおりサーバーを通らないため、独立して理解する価値があります。代表的なsource/sinkを並べます。

| 汚染source（クライアント） | 危険sink | 結果 |
|---|---|---|
| `location.hash` / `location.search` | `element.innerHTML` | DOM-XSS |
| `URLSearchParams.get()` | `document.write()` | DOM-XSS |
| `document.referrer` | `eval()` / `new Function()` | 任意コード実行 |
| `postMessage` の `event.data` | `location.href = ...` | 誘導 / XSS |
| `localStorage` の値 | `setTimeout(文字列)` | 任意コード実行 |

ありがちなのが、URLハッシュの値を画面に書き戻すパターンです。

```tsx
// 脆弱：URLハッシュの値をそのままHTMLに書き戻す（DOM-XSS）
useEffect(() => {
  const tab = decodeURIComponent(location.hash.slice(1)); // ← 汚染source
  document.querySelector("#title")!.innerHTML = `タブ: ${tab}`; // ← 危険sink
}, []);
```

`location.hash`（`#` 以降）は**サーバーへ送信されません**。だからSSRのHTMLにもサーバーログにも現れず、WAFも検知できません。修正は「DOMへ直接書かず、Reactのstate経由で描画する」——これだけで自動エスケープの傘の下に戻れます。

```tsx
// 修正：DOMに直接書かず state 経由で描画する
const [tab, setTab] = useState("");
useEffect(() => {
  setTab(decodeURIComponent(location.hash.slice(1)));
}, []);
return <p id="title">タブ: {tab}</p>;
```

`eval` / `new Function` / 文字列を渡す `setTimeout` は、そもそも使わないのが最善です。設定値を「コードとして」評価する設計をやめ、データとして扱えば、このクラスのsinkは消えます。

---

## 7. 出力時サニタイズ：DOMPurifyと「そもそも入れない」設計

どうしても信頼できないHTMLを描画する必要があるなら、**DOMPurify** で無害化します。Next.jsはサーバーとクライアントの両方で描画されるため、`isomorphic-dompurify`（サーバーでは内部的にDOM実装を使う）を選ぶと両環境で動きます。

```tsx
// components/rich-text.tsx — 出力の直前にサニタイズし、許可を「狭く」明示する
import DOMPurify from "isomorphic-dompurify";

export function RichText({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["p", "br", "b", "i", "em", "strong", "ul", "ol", "li", "a"],
    ALLOWED_ATTR: ["href"], // 属性は最小限に
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
```

設計上、外さないでほしい原則が3つあります。

1. **入力時ではなく出力時にサニタイズする。** 同じデータが複数の文脈（HTML本文・属性・URL）で再利用されることがあり、サニタイザの改善は過去の保存データには遡及せず、入力フィルタは別経路の保存で簡単に迂回されます。「描画する場所」でかけるのが最も確実です。
2. **allowlistは狭く、明示する。** 「危険なものを除く（denylist）」ではなく「許すものだけ通す（allowlist）」。新しい攻撃ベクタが見つかっても、許可を絞っていれば影響を受けにくくなります。
3. **最強の対策は「生HTMLを文字列で持たないこと」。** ユーザー投稿のリッチテキストなら、HTML文字列ではなく**Markdownや構造化ブロック**として保存し、安全なレンダラで描画すれば、サニタイズすべきHTML文字列がそもそも存在しません。たとえばこのブログは `react-markdown` で描画していますが、**デフォルトでは生HTMLを解釈しません**。`rehype-raw` を足した瞬間に、その安全性は失われます。

> **正直なスコープ。** DOMPurifyは強力ですが、「魔法の安全装置」ではありません。allowlistが描画文脈に合っていなければ穴は残りますし、`SVG` や `MathML` を許可すれば検討すべき表面が増えます。**何を許可するかの判断は、あなたのプロダクトの要件に依存する設計判断**であり、ライブラリは肩代わりできません。

---

## 8. Trusted TypesとCSP(nonce)：多層防御の「最後の壁」

ここまでは「穴を開けない／無害化する」一次防御でした。次は、**万一それをすり抜けても被害を抑える**多層防御です。一次防御が完璧である保証はないという前提に立つのが要点です。

### Trusted Types：危険シンクを型で縛る

Trusted Typesは、`innerHTML` のような危険シンクへの**素の文字列代入を実行時に禁止**するブラウザの仕組みです。CSPで有効化すると、シンクには登録済みポリシーが生成した `TrustedHTML` しか渡せなくなり、監査すべき箇所が「数個のポリシー」に集約されます。

```ts
// CSP: require-trusted-types-for 'script'; trusted-types app-html;
// 有効化すると innerHTML 等への素の文字列代入が TypeError になる
import DOMPurify from "isomorphic-dompurify";

// 型は @types/trusted-types を導入して補う（any を使わない）
if (typeof window !== "undefined" && "trustedTypes" in window) {
  window.trustedTypes!.createPolicy("app-html", {
    createHTML: (input: string) => DOMPurify.sanitize(input),
  });
}
```

`require-trusted-types-for 'script'` の指定は [MDN: Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) が一次情報です。ただし**ブラウザ対応は均一ではありません**（Chromium系で先行、他は限定的）。だからこれは「最後の壁」であって、対応ブラウザだけを守る前提にはできません。多層防御の1枚として積む位置づけです。

### CSP(nonce)：注入されても実行させない

CSP（Content Security Policy）は、ページが読み込めるスクリプトの出所を制限します。`'unsafe-inline'` を避け、**リクエストごとにnonceを発行**して `script-src 'self' 'nonce-...' 'strict-dynamic'` とすれば、万一インラインスクリプトを注入されても実行されません。

```ts
// CSP の考え方（nonce発行とミドルウェア実装の詳細は別記事）
const csp = [
  "default-src 'self'",
  `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
  "object-src 'none'",
  "base-uri 'none'",
].join("; ");
```

ここで強調すべき正直さがあります。**CSPはXSSの「注入」を防ぐものではなく、注入された後の「実行・悪用」を制限するものです。** そして `'unsafe-inline'` を付けたり、`script-src` をワイルドカードで広げたりした緩いCSPは、**油断を生むだけで防御になりません。** nonce発行とミドルウェアでの厳格な実装は[Next.jsのセキュリティヘッダーとCSP(nonce)](/blog/nextjs-security-headers-csp-nonce-middleware-guide)に切り出しています。

---

## 9. 検出：汚染入力からDOMシンクへの流れを追う

設計で塞ぐと決めたら、「塞げているか」を検証します。XSSの多くは構造が共通なので、機械的に検出できる部分があります。

- **すべての `dangerouslySetInnerHTML` を洗い出す** — 出力がサニタイズを通っているかをレビュー対象に固定する。
- **汚染source→危険sinkのデータフローを追う（taint解析）** — `location.*` / `searchParams` / リクエスト由来の値が、`innerHTML` / `document.write` / `eval` に検証なしで到達していないか。
- **`href` / `src` の動的値**にスキーム検証が入っているか。
- **`eval` / `new Function` / 文字列 `setTimeout`** の使用箇所。

これらは正規表現では正確に書けません。「値がどこから来てどこへ流れるか」を追う必要があるからです。私が公開しているOSS [Aegis](/aegis) はこのデータフロー検出を実装しており、インストール不要で走ります。

```bash
# インストール不要・設定不要でスキャン（汚染入力→DOMシンクを可視化）
npx @aegiskit/cli scan
```

手動の検証手順としては、OWASPの [Web Security Testing Guide](https://owasp.org/www-project-web-security-testing-guide/) がXSS（反射型・格納型・DOM）のテスト観点を体系化しています。スキャンの「疑い」を、実際に自分のアプリで再現して確定させるのが王道です。

> **正直なスコープ。** 検出できるのは「危険シンクに汚染入力が届いている」「サニタイズを通っていない」という**形**までです。**「そのHTMLの出所は本当に信頼できるのか」「サニタイザのallowlistはこの描画文脈に合っているか」「出力エンコーディングは文脈（HTML本文／属性／URL／JS）に正しく対応しているか」**——これら安全な出力設計の妥当性は、ツールには判定できません。データフロー解析も関数内が基本で、モジュールを跨ぐ流れは見逃します。**検出は人間のレビューを置き換えるのではなく、補完するもの**です。

---

## 10. 本番前チェックリスト

外注でもAI製でも、本番投入の前に最低限これだけは確認してください。

- [ ] `dangerouslySetInnerHTML` の各箇所で、HTMLの**出所が信頼できる**か、または**出力時サニタイズ**を通している
- [ ] サニタイザは**allowlist方式**（許可するタグ・属性を明示）になっている
- [ ] `href` / `src` の動的値は、**URL APIでスキームを検証**している（正規表現自作に頼らない）
- [ ] `ref` 経由の `innerHTML` 代入や `document.write` を使っていない（テキストは `textContent` / state へ）
- [ ] `location.hash` / `searchParams` 等の**汚染sourceがDOMシンクに素通り**していない
- [ ] `eval` / `new Function` / 文字列を渡す `setTimeout` を使っていない
- [ ] ユーザー投稿は、生HTMLではなく **Markdown / 構造化データ**として保持している（`rehype-raw` 等で生HTMLを再有効化していない）
- [ ] **CSP(nonce)** を付与し、`'unsafe-inline'` やワイルドカードで緩めていない
- [ ] 余力があれば **Trusted Types** を有効化し、危険シンクをポリシー経由に絞っている
- [ ] **taint/DOMシンク検出**をCIに常設している（`npx @aegiskit/cli scan`）

発注者の視点で効くのは、**「`dangerouslySetInnerHTML` は何箇所ありますか？」「そのHTMLの出所は誰が書けますか？」「URLのスキームはどこで検証していますか？」**の3問です。良い開発者は即答できます。

---

## 11. どこまで自分で、どこから監査か

最後に、正直に線を引きます。

**サニタイズ忘れ・汚染→シンクの流れ・`javascript:` スキームの素通りは、静的解析で機械的に検出できます。** ここはCIに番をさせるべき領域で、人間が毎回目視する必要はありません。まずは [Aegis](/aegis)（無料OSS、`npx @aegiskit/cli scan`）で現状を可視化するのが、最もコスパの良い第一歩です。

一方、**「そのHTMLは信頼できるのか」「allowlistはこの文脈に合っているか」という安全な出力設計の妥当性は、人間の判断に残ります。** ここでツールが「安全だ」と言い切る製品は、むしろ危険です。**Aegisは危険な出力経路を検出・警告しますが、出力設計が正しいことを証明はしません。完全に安全にする魔法はありません。** だからこそ最初の可視化（無料OSS、`npx @aegiskit/cli scan`）で穴の在処を掴むことから始めるのが、最もコスパの良い第一歩です。

だからこそ、線引きが要ります。どこまで自分で固め、どこから専門家のレビューが要るか——既存アプリのXSS・出力安全性のレビューや、CSP・Trusted Typesを含む多層防御の設計が必要なら、[セキュリティ監査](/aegis/audit)で承ります。私自身、[木材流通業界のDX案件](/case-studies/lumber-industry-dx)で、信頼境界をまたぐデータの出力安全性を実運用で設計・検証してきました。

---

## よくある質問（FAQ）

**Q. Reactを使っていればXSSは起きませんか？**
A. 素直に書いている限り、Reactの自動エスケープが大半を防ぎます。ただし `dangerouslySetInnerHTML`、`href` の `javascript:`、`ref` 経由の `innerHTML`、DOM-XSSは、その防御の「外側」です。フレームワークの安全性は、安全弁を自分で外さない限りで成り立ちます。

**Q. `dangerouslySetInnerHTML` は使ってはいけませんか？**
A. 禁止ではありません。**出所が完全に信頼できるHTML**（サーバーで自分が組み立てたJSON-LD等）に限れば適切な用途です。リクエスト・DB・外部由来の値が混ざる場合は、出力時にDOMPurifyでサニタイズしてください。

**Q. 入力時にサニタイズしておけば十分ですか？**
A. 不十分です。同じデータが別の文脈で再利用されたり、サニタイザの改善が過去データに遡及しなかったり、別経路の保存で入力フィルタを迂回されたりします。サニタイズは**描画する出力時**にかけるのが最も確実です。

**Q. CSPを入れればXSSは防げますか？**
A. CSPは「注入を防ぐ」ものではなく「注入された後の実行・悪用を制限する」多層防御です。しかも `'unsafe-inline'` などで緩めると効果を失います。一次防御（サニタイズ・スキーム検証）と組み合わせる「最後の壁」と考えてください。

**Q. DOM-XSSはサーバー側の対策で防げますか？**
A. 防げません。`location.hash` のようにサーバーへ送られない値が起点になることが多く、SSRのサニタイズもWAFも届きません。クライアントで汚染sourceを危険sinkに渡さない（state経由・`textContent`）設計が必要です。

---

## まとめ：守りの境界は「エスケープの有無」から「出力設計の正しさ」へ

要点を整理します。

- Reactは **JSXの式展開を自動でHTMLエスケープ**するためXSSに強い。穴は攻撃者ではなく**開発者が開ける**——`dangerouslySetInnerHTML`、`javascript:` スキーム、`ref` 経由の `innerHTML`、サードパーティHTMLの4経路。
- **DOM-XSS**はサーバーを介さずブラウザ内で完結し、サーバーログにもWAFにも映りにくい。汚染source→危険sinkの流れを、state経由・`textContent`で断つ。
- 信頼できないHTMLは**出力時にDOMPurifyでallowlistサニタイズ**。最も堅いのは**生HTML文字列を持たない**（Markdown・構造化データ）設計。
- **Trusted Types / CSP(nonce)** は、混入しても被害を抑える多層防御。緩いCSPは油断を生むだけで防御にならない。
- サニタイズ忘れや汚染→シンクの流れは**機械的に検出できる**。だが**安全な出力設計の妥当性は、設計と人間のレビューでしか守れない**。

AIで速く作ること自体は正しい。**速く作ったものを、漏らさず安全に固める**——その仕組みづくりや、既存のNext.js / Reactアプリの出力安全性レビューが必要であれば、お気軽にご相談ください。

---

## 参考資料

- [OWASP Top 10（XSSはA03:2021 Injectionに分類される）](https://owasp.org/www-project-top-ten/)
- [MDN — Content-Security-Policy（CSP・require-trusted-types-forの一次情報）](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
- [OWASP Web Security Testing Guide（反射型・格納型・DOM-XSSのテスト観点）](https://owasp.org/www-project-web-security-testing-guide/)
