Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ソースコードで比較する React / Vue / Svelte の セキュリティ設計思想 /...

ソースコードで比較する React / Vue / Svelte の セキュリティ設計思想 / security design philosophy react vue svelte

More Decks by コドモン開発チーム

Transcript

  1. ソースコードで比較する React / Vue / Svelte の セキュリティ設計思想 FRONTEND CONFERENCE

    NAGOYA 2026 株式会社コドモン 塚原 彰仁 #fec_nagoya 01 / 50
  2. 自己紹介 塚原 彰仁 Akito Tsukahara 株式会社コドモン ソフトウェアエンジニア X: AkitoTsukahara GitHub:

    AkitoTsukahara 共著書 プロフェッショナル Webプログラミング Laravel 改訂版 良かったら読んでね! Zenn で公開した React・Vue・Svelte の XSS 対策比較記事 を実機検証と内部実装で深掘りしたのが 本発表です。 #fec_nagoya 02 / 50
  3. WEBアプリケーションエンジニア • ユーザーからのフィードバックをもとにした機能改善 • レガシーコードのリファクタリングやリプレイス • 新機機能や新規プロダクトの開発 • 開発生産性向上 QAエンジニア

    • 品質保証戦略の策定 • 品質に関する知識・技術の普及推進 • ソースコード品質向上のためのCI/CDパイプライン構築 • テスト設計・実装 エンジニアリングマネージャー • 採用・評価運用の改善の推進 • 開発ロードマップの議論の推進 • 開発部全体のプロセス改善の推進 • 自己組織化の促進 やることの例 やることの例 やることの例 色々な職種・役割において仲間を募集中です 詳細な求人情報は コドモン採用ページから ご確認下さい!
  4. 本日話すこと 01.動機と前提 02.トピック 1: Raw HTML 挿入 03.トピック 2: javascript:

    URL 04.トピック 3: コンパイル時 vs ランタイム 05.設計思想の比較とまとめ ※ 想定Q&A、おまけスライドも用意しました! #fec_nagoya 03 / 50
  5. 本日話さないこと 本編に集中するため、以下は今回のスコープ外 XSS以外の セキュリティ領域 CSRF / 認証 / Cookie /

    セッション管理 01 メタフレームワークの 領域 Next.js / Nuxt / SvelteKit おまけスライドで少し触れる 02 どのFWが「正しい」 かの価値判断 本トークは「異なる正解」 を読み解くトーク 03 #fec_nagoya 04 / 50
  6. 比較のアプローチ (1/3) — 観点1 異なる「防御ポイント」のセキュリティ実装を比較する ブラウザ仕様 innerHTML 経由の <script> は

    実行されない CSP:script-src インラインイベントハンドラ ・javascript: URL をブロック CSP:Trusted Types innerHTML への文字列代入自体 をブロック 同じXSSリスクでも、どのポイントで対処するかで設計思想の差分が見える #fec_nagoya 06 / 50
  7. 比較のアプローチ (2/3) — 観点2 創設者の Issue・PR・コメントから推察する GitHub Issue / PR

    作者本人のコメントから、設計判断の意図を読み取る ・React PR #15047: javascript: URLブロック ・Svelte Issue #2545: @html改名議論 設計議論 創設者間の議論から、設計思想の背景を理解する ・Rich Harrisの発言 ・Evan Youの設計判断 推察のポイント ・なぜその設計になったか 技術的背景を理解 ・どの問題を解決したいか 設計意図を把握 ・トレードオフの判断 設計思想を読み取る #fec_nagoya 07 / 50
  8. 比較のアプローチ (3/3) — 観点3 公式ドキュメントとFWの起源から補強する 公式ドキュメント 各フレームワークの公式ガイド・Design Principlesから、 設計思想の根拠を読み取る React:

    Design Principles Vue: Style Guide Svelte: Docs FWの起源 各フレームワークの誕生背景から、設計判断の意図を理解す る React: Facebook大規模開発の中で生まれた Vue: 個人開発から出発 Svelte: Web記事で図表の制作の経験から生まれた 公式ドキュメントと起源の背景から、設計思想と FW の存在目的を推察する #fec_nagoya 08 / 50
  9. 本日扱う 3 つのトピック 3 つの切り口で、設計思想の差を見る TOPIC 1 Raw HTML 挿入

    dangerouslySetInnerHTML · v-html · {@html} Runtime DOM 書き込み時 TOPIC 2 javascript: URL <a href="javascript:<<."> 属性バインディング時 TOPIC 3 コンパイル時 vs ランタイム ランタイムの厚みと、フッ クを置ける場所 09 / 50 #fec_nagoya
  10. 3つのフレームワークで書いてみると React ./ userInput = '<img src=x...' <div dangerouslySetInnerHTML={{ <_html:

    userInput }} <> Vue ...- userInput = '<img src=x...' ..> <div v-html="userInput"<> ...- userInput = '<img src=x...' ..> {@html userInput} Svelte 11 / 50 #fec_nagoya
  11. 実機で動かしてみました 検証環境 React 19.2.5 | Vue 3.5.32 | Svelte 5.55.4

    検証ペイロード <img src=x onerror="alert(XSS via onerror)"> 検証コード デモリポジトリ https://github.com/AkitoTsukahara/fec-nagoya-2026-demo.git 3フレームワーク とも、これで XSS が発火することが検証できます 12 / 50 #fec_nagoya
  12. 結果:3FW ともアラートが発火 React 19.2.5 発火 🔥 Vue 3.5.32 Svelte 5.55.4

    「HTML として挿入する」書き方 である以上、 <img onerror> が DOM に入り、 画像読み込み失敗で発火する。 発火 🔥 発火 🔥 13 / 50 #fec_nagoya
  13. Reactの内部実装 react-dom-bindings/src/client/ReactDOMComponent.js if (nextHtml <= null) { setInnerHTML(domElement, nextHtml); }

    さらに setInnerHTML の中身: setInnerHTML.js node.innerHTML = html; 最終的に element.innerHTML への代入 15 / 50 #fec_nagoya
  14. Vueの内部実装 runtime-dom/src/modules/props.ts — patchDOMProp() if (key <<= "innerHTML") { el[key]

    = value <= null ? "" : unsafeToTrustedHTML(value); } 最終的に el.innerHTML への代入 16 / 50 #fec_nagoya
  15. Svelteの内部実装 packages/svelte/src/internal/client/dom/blocks/html.js exportfunctionhtml(node, get_value, svg, mathml) { // //. template.innerHTML

    = svg ? `<svg>${value}</svg>`: mathml ? `<math>${value}</math>`: value; } 最終的に template.innerHTML への代入 17 / 50 #fec_nagoya
  16. 違いは「危険性の見せ方」にある React 命名と形式で 抑止する dangerouslySetInnerHTML + オブジェクト形式 {<_html:} Vue ガイドで

    警告する v-html + 公式ガイド Svelte 警告は ESLintで {@html} + ESLintプラグイン 警告をどこに置くか、3FWで違う #fec_nagoya 19 / 50
  17. React:なぜ「dangerously」と名付けたのか "...you have to type out dangerouslySetInnerHTML and pass an

    object with a ._html key, to remind yourself that it's dangerous." — React 公式ドキュメント(legacy.reactjs.org) 偶然ではなく、設計意図 20 / 50 (dangerouslySetInnerHTML と書いて、__html キーを持つオブジェクトを渡す  ——危険を自分に思い出させるために) #fec_nagoya
  18. 背景:Facebook大規模開発の文化 "we value distinct verbose names, and especially for the

    features that should be used sparingly. For example, dangerouslySetInnerHTML is hard to miss in a code review." — React Design Principles(Optimized for Tooling) ありがちなミスを構文の段階で防ぐ 21 / 50 (特に控えめに使うべき機能には、目立つ冗長な名前をつけることを大切にしている。  例えば dangerouslySetInnerHTML は、コードレビューで見逃しようがない) #fec_nagoya
  19. React設計判断:ミス防止型の設計 Reactの環境 数千人のFacebookコードベース Reactは「書き手のミス」を防ぐ設計 スキルや経験の幅が広い 大規模なコードレビューが必要 ミスの影響が大きい Sebastian Markbågeの設計判断 ありがちなミスを構文の段階で防ぐ

    設計意図:ミスを「見逃せない」構文にする 長い名前はレビューで見逃せない dangerouslySetInnerHTML {<_html:} のオブジェクト形式 ミス防止型の設計:「危険であることを思い出させる」命名 → 数千人のエンジニアが安全にコードを書けるように 22 / 50 #fec_nagoya
  20. 背景:Vueの起源とEvan You "what if I could just extract the part

    that I really liked about Angular and build something really lightweight" (Angularの好きな部分だけを抽出して、本当に軽量なものを作れたら) — Evan You, Between the Wires(2017年) → 軽量さから始める progressive framework 23 / 50 #fec_nagoya
  21. Vue:アーキテクチャで対処する立場 "It is impractical for a framework like Vue to

    completely shield you from potential malicious code execution without incurring unrealistic performance overhead." (フレームワークが完全に守るのは、非現実的なパフォーマンスコストなしには不可能) — Vue Security Guide → フレームワーク単独では守り切れない 24 / 50 #fec_nagoya
  22. Vue設計判断:progressive framework Vueの環境 個人開発から始まったview層フレームワーク Evan Youの個人開発から出発(2014) Angularの「いいとこ取り」 段階的に採用できる柔軟性 Evan Youの設計判断

    軽量なコアから出発、層で協調する view層に絞り、他はエコシステムに任せる フレームワーク単独では守り切れない progressive framework という思想 progressive framework:軽量さから始め、層で協調する設計 Vueは段階的に始められる軽量さ 設計意図:協調する設計を 25 / 50 #fec_nagoya
  23. Svelte:「@unsafeHTML」にすべきか議論 "I think warning fatigue is a real thing. Most

    of the time when I'm using {@html ...} it's with data coming from a place that I know is safe." — Rich Harris, sveltejs/svelte#2545 結論:改名しない 26 / 50 (警告疲れというのは現実にある。  {@html ...} を使うときの大半は、安全だと分かっているデータが相手なんだ) #fec_nagoya
  24. 背景:Svelteの「シンプルさ優先」 "Reducing the amount of code you have to write

    is an explicit goal of Svelte." (書くコード量を減らすことは、Svelteの明示的な目標) — Rich Harris, "Write less code"(svelte.dev/blog) シンプルさこそがSvelteの本質 27 / 50 #fec_nagoya
  25. Svelte設計判断:シンプルさ優先 Svelteの環境 HTML/CSS から始める書き手を想定 Rich Harris は The Guardian /

    NYT 出身 インタラクティブジャーナリズムの現場 HTML を書ける人なら踏み込める設計 Svelteは「書き手のシンプルさ」を優先する設計 Rich Harrisの設計判断 書き手の負担を増やさない判断 実行時警告は最小限にとどめる 不足は ESLint プラグインで補う "warning fatigue is a real thing" 設計意図:シンプルさを最優先する シンプルさ優先:書き手の体験を最優先する設計 → HTML/CSSから始める書き手向け #fec_nagoya 28 / 50
  26. トピック1まとめ — Raw HTML 挿入 dangerouslySetInnerHTML · v-html · {@html}

    共通点 : 最終的に innerHTML への代入 React Vue Svelte 書き方 dangerouslySetInnerHTML v-html {@html} 起源 Facebook大規模開発 個人開発の実用性 HTML/CSSから 始めるシンプルさ 設計判断 命名と形式で抑止する Docで警告する ESLintで補う 設計思想 ミス防止型 progressive framework シンプルさ優先 各FWの起源と設計思想から自然に出てきた「異なる正解」 29 / 50 #fec_nagoya
  27. javascript: URLとは <a href = "javascript:alert(1)">リンクをクリック </a> クリック href のJavaScriptが実行される

    alert(1) 発火 XSSのベクターとして ⚫ ユーザー入力を href に入れると危険 ⚫ http:</ や https:</ 以外が入りうる ⚫ レガシーだが、今も普通に動く 31 / 50 #fec_nagoya
  28. 3FWでjavascript: URLを試すと const url="javascript:alert(1)"; <ahref={url}>クリック</a> React 19 ブロック ✋ (クリック時に

    throw) React 18 警告のみ 素通し ⚠ Vue 実行される ⚠ Svelte 実行される ⚠ Uncaught Error: React has blocked a javascript: URL as a security precaution. — React 19 のクリック時エラー 32 / 50 #fec_nagoya
  29. Reactの内部実装:sanitizeURL.js react-dom-bindings/src/shared/sanitizeURL.js const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[ \r\n\t]*t[\r\n\t]*:/i; 正規表現の解読 ^[\u0000-\u001F

    ]* 先頭の制御文字・空白 j, a, v, a, s, c, r, i, p, t, : [\r\n\t]* 各文字間の改行・タブもOK 意図 攻撃者があらゆる方法でエスケープを試みるパ ターンを全部カバーする堅牢な正規表現 NOTE Reactのこの防御は "href" 属性など一部経路のみ。window.location = userInput のような Reactが関与しない経路までは守れない。 33 / 50 #fec_nagoya
  30. Reactはなぜブロックを選んだのか PR #15047 "JavaScript URLs is also not really a

    legit use case once you're on the client in React since you can just attach event listeners and preventDefault instead." (クライアントのReactでJavaScript URLを使う正当な理由はない。イベントリスナーと preventDefaultで代替できるから) — Sebastian Markbåge, React PR #15047 正当な使用例 ほぼない 代替手段 onClickハンドラで十分 リスク XSSベクター 「フレームワークとして書き手のミスを防ぐ」という一貫性 #fec_nagoya 34 / 50
  31. Vue・Svelteはなぜブロックしないのか Vue "if you're ever doing URL sanitization on the

    frontend, you already have a security issue." (フロントエンドでURLサニタイズしている時点で、 すでにセキュリティの問題) — Vue Security Guide アプリケーション側で検証することを推奨 sanitize-url などを紹介 Svelte 明示的なブロック機構なし ESLint の no-script-url などで検出可能 シンプルさを保つ選択 React: フレームワークで守る |Vue: 協調する層の1つ |Svelte: ESLintで補う 同じリスクへの、異なるアプローチ 35 / 50 #fec_nagoya
  32. トピック2まとめ javascript: URL ▪共通点 javascript: URLはXSSベクター。どのFWでも検証すべき入力 ▪設計判断 React フレームワークで守る Vue

    協調する層の1つ Svelte ESLintで補う 設計思想がそのまま、実装に表れている 36 / 50 #fec_nagoya
  33. 最後に進め方を変えてみます 📝 これまでのトピック1・2 具体的な実装 dangerously<<. / v-html / {@html} 内部実装

    設計思想 ミス防止型 / progressive / シンプル "具体 → 抽象" 実装から思想を辿る 🏛 これからのトピック3 アーキテクチャ 仮想DOM の有無 ランタイムの厚み 防御フックの場所 集約するか、しないか "抽象 → 具体" 思想から実装を見る トピック3 では、仮想DOM の有無というアーキテクチャから出発します 38 / 50 #fec_nagoya
  34. ソースコードからDOMまでの流れ — PIPELINE 上レーン:仮想DOMあり (React/Vue) ランタイム集約 ★主に使う JSX/ テンプレート 仮想DOM

    コンパイル Reactivity 差分計算 DOM API 呼び出し Browser DOM 下レーン:仮想DOMなし (Svelte) コンポーネント テンプレート コンパイラで DOM命令へ展開 ランタイム 実行 Browser DOM トピック3:仮想DOMの有無が、防御フックを差し込む自然な場所を変える コンパイル時解析 ★主に使う ランタイム介入 △限定的 39 / 50 #fec_nagoya
  35. 3FWの「ランタイム」の厚みが違う(≒FW がブラウザ上で動かす処理の量) React JSXはcreateElement()呼び出 しにトランスパイル。 propsの処理・DOMへの反映は 全部ランタイム。 sanitizeURLを ランタイムで挟める 厚いランタイム

    Vue テンプレートはrender関数にコ ンパイルされるが、リアクティ ブ追跡・差分適用はランタイ ム。 Trusted Typesも ランタイム委任 厚いランタイム Svelte コンパイル時に必要最小限の DOM命令へ展開。ランタイムは ごく薄い(disappearing frameworkと呼ばれる)。 共通関門に 集約しない設計 薄いランタイム 仮想DOM の有無がランタイムの厚みを決め、防御の置き場所を決める → 厚い:集約する設計 / 薄い:集約しない設計 #fec_nagoya 40 / 50
  36. コンパイル時の防御 ―― Svelteコンパイラの警告 Svelte コンパイラは静的解析で javascript: URL を検出する A —

    静的な値(コンパイル時に判明) ///- 静的な値 //> <a href="javascript:alert(1)">クリック</a> 警告:a11y_invalid_attribute (内部的には /^\W*javascript:/i で判定) B — 動的な値(コンパイル時には不明) ///- 動的な値 //> <a href={userInput}>クリック</a> 警告は出ない 変数バインディングは静的解析の対象外 Svelte は「集約しない」代わりに、コンパイル時にできることはやる 「コンパイル時 vs ランタイム」の境界が、Svelte の防御戦略を決める Akito Tsukahara 41 / 50 #fec_nagoya
  37. React・Vue は「ランタイムに集約する」 React createElement → 属性処理パス function setAttr(node, name, value)

    { if (isURL(name)) value = sanitizeURL(value); node.setAttribute(name, value); } 属性代入が通る一点に集約する設計 Vue render 関数 → patchProp function patchProp(el, key, val) { if (key <<= 'innerHTML') val = unsafeToTrustedHTML(val); el[key] = val; } Trusted Types 対応も 同じ一点に集約する設計 ランタイムが厚い = DOM 書き込みを一点に集約する設計を選んでいる ミス防止・協調という思想と整合する 42 / 50 #fec_nagoya
  38. Svelte 5.52:{@html} が TrustedHTML をサポート </ ユーザーが明示的に書く (Svelte) {@html policy.createHTML(html)}

    Svelte 5.52以降(2026年リリース)PR #17701(Rich Harris) ▪設計が React・Vue とは異なる React |  Vue フレームワークが自動で TrustedHTML 化 val=unsafeToTrustedHTML(value); val = unsafeToTrustedHTML(value); Svelte ユーザーが明示的に書く 薄いランタイムでも対応可能 Svelte 5.52 でも「ユーザー明示」を一貫 — シンプルさ優先の思想と整合 43 / 50 #fec_nagoya
  39. トピック3まとめ — アーキテクチャの選択が、設計思想と整合する形で実装    に表れる React /  Vue 集約する設計を選択 •

    属性パスに集約(sanitizeURL 自動) • patchProp に集約(Trusted Types 自動) Svelte 集約しない設計を選択 • html.js で受ける(ユーザー明示) • コンパイル時に必要最小限の DOM 命令へ展開 アーキテクチャの選択が、設計思想と整合する形で実装に表れる React/Vue ランタイムが厚い=DOM 書き込みを一点に集約 できる → セキュリティフックが挟みやすい Svelte ランタイムが薄い=ユーザー側で明示的に対応 → シンプルさを優先 44 / 50 #fec_nagoya
  40. 設計思想まとめ React 命名と形式で抑止 +フレームワークで守る 数千人のFacebook Sebastian Markbåge ミス防止型 Vue アーキテクチャで対処

    個人開発の実用性 Evan You 協調型 Svelte シンプル+ ESLintで補う HTML/CSSから始める Rich Harris シンプル型 46 / 50 #fec_nagoya
  41. 実装から見えてきたものは 1 同じリスクに、違うアプローチが取られている トピック1・2 を通じて、Raw HTML 挿入や javascript: URL という同じ

    XSS リスクに対して、 3FW は異なる実装を選んでいることを見てきた 2 その違いは、設計思想から繋がっている 命名、API、ブロック挙動の違いは、それぞれのFW が持つ設計思想 (ミス防止型 / 協調型 / シンプル型)から繋がっている 3 アーキテクチャの選択も、設計思想と整合している 仮想DOM の有無とランタイムの厚みは、防御フックの場所を変える その選択も、各FW の設計思想と整合する形で実装に表れている 47 / 50 #fec_nagoya
  42. 参考資料 (1/2) 公式ドキュメント React 公式ドキュメント https:</react.dev/learn/javascript-in-jsx-with-curl… Vue 3 公式ガイド https:</vuejs.org/guide/essentials/template-syntax.html

    Svelte 5 ドキュメント https:</svelte.dev/docs/svelte/basic-markup GitHub ソースコード facebook/react https:</github.com/facebook/react vuejs/core https:</github.com/vuejs/core ブラウザ仕様・Web 標準 HTML Standard https:</html.spec.whatwg.org CSP Level 3 https:</www.w3.org/TR/CSP3/ Trusted Types https:</w3c.github.io/trusted-types/ sveltejs/svelte https:</github.com/sveltejs/svelte 48 / 50 #fec_nagoya
  43. 参考資料 (2/2) GitHub PR / Issue React PR #15047 javascript:

    URL ブロック https:</github.com/facebook/react/pu… Svelte PR #17701 {@html} の TrustedHTML サポート(5.52) https:</github.com/sveltejs/svelte/pu… Svelte Issue #2545 @html 改名議論 https:</github.com/sveltejs/svelte/i… 創設者インタビュー・発言 Rich Harris × Smashing Magazine (2025) Svelte 5の未来とフレームワークの進化 https:</www.smashingmagazine.com/2025/01/svelte-5-futu… Evan You × Between the Wires (2017) Vue.jsの起源と設計思想 https:</www.freecodecamp.org/news/between-the… 元記事・デモ Zenn 元記事 React/Vue/SvelteのXSS対策比較記事 https:</zenn.dev/akito_tsukahara/articles/1d15cb875b0f82 Demo リポジトリ 実機検証用のデモコード https:</github.com/AkitoTsukahara/fec-nagoya-2026-demo 49 / 50 #fec_nagoya
  44. B-01 用語解説 (1/2):DOM 操作とセキュリティレイヤー innerHTML HTMLとして解析・実行されるため、XSSのリスクが高いプロパティ。 element.innerHTML = input; textContent

    プレーンテキストとして扱われるため安全だが、HTMLタグは機能しない。 element.textContent = input; TrustedHTML 信頼できるHTMLオブジェクトのみを許可する仕組み。CSPと併用で効果 を発揮する。 const trusted = policy.createHTML(html); element.innerHTML = trusted; セキュリティレイヤー [入力層] ユーザー入力 <script>alert(1)</script> ↓ [検証層①] サニタイズ処理 DOMPurify.sanitize() ↓ [検証層②] TrustedHTML検証 policy.createHTML() ↓ [ブラウザ層] CSP検証 require-trusted-types-for ↓ [DOM層] DOM挿入 innerHTML 代入 B-02 /10 #fec_nagoya
  45. B-02 用語解説 (2/2):CSP・サニタイズ・ブラウザ機能 CSP (Content Security Policy) ブラウザレベルのセキュリティ制御。HTTP レスポンスヘッダーで配信 し、意図しないスクリプトの実行やリソース読み込みをブラウザに拒否さ

    せる。 Content-Security-Policy: script-src 'self'; require-trusted-types-for 'script'; サニタイズ (Sanitization) ユーザー入力を安全な形式に変換する処理。<script> タグやイベントハン ドラ属性(onerror など)を取り除く。HTML 用と URL 用でライブラリが 異なる点に注意。 </ 危険な入力 <img src="x" onerror="alert(1)"> </ サニタイズ後 <img src="x"> サニタイズライブラリ (npm パッケージとして配布される実装手段) DOMPurify HTML 向けのサニタイザ。SVG・MathML もサポート。 React/Vue/Svelte いずれでも dangerouslySetInnerHTML / v-html / {@html} の前段で使われる定番。 DOMPurify.sanitize(html) @braintree/sanitize-url URL 向けのサニタイザ。javascript:vbscript: などの危険なプロトコ ルを無害化。Vue 公式の Security ガイドでも推奨。 sanitizeUrl(url) ブラウザ機能 (ブラウザに組み込まれた標準 API) Trusted Types ブラウザ標準 API。文字列の innerHTML 代入をブロックし、ポリシー経 由の TrustedHTML オブジェクトのみを許可する仕組み。2026 年 2 月に Firefox / Safari もサポート。 trustedTypes.createPolicy(<<.) B-03 / 10 #fec_nagoya
  46. B-03 innerHTML の先にある多層防御 本編で「危険な API」と紹介した dangerouslySetInnerHTML / v-html / {@html}

    を、 それでも使ってしまった場合に備えるブラウザ側の 3 レイヤー LAYER 1 ブラウザ仕様(HTML Standard) innerHTML 経由の <script> は実行されない仕様 ただし <img onerror> や <iframe srcdoc> 経由のイベントハンドラは発火する LAYER 2 CSP:script-src unsafe-inline を許容しないポリシーなら、 インラインイベントハンドラと javascript: URL をブロック LAYER 3 CSP:require-trusted-types-for innerHTML への文字列代入自体をブロック TrustedHTML オブジェクトのみを受け付ける(ポリシー経由でのみ生成可能) B-04 / 10 #fec_nagoya
  47. B-04 自動エスケープの内部実装:3FW のソースコードを覗く 3FW とも、最終的にはブラウザ標準の textContent / nodeValue / createTextNode

    を呼ぶ これらは「HTML として解釈しない」DOM API で、<script> も単なる文字列のまま画面に出る React // react-dom-bindings/src/client/ // setTextContent.js function setTextContent(node, text) { node.textContent = text; // or node.nodeValue = text } ランタイムで setTextContent を呼 び、ブラウザ標準の textContent ま たは nodeValue に文字列を代入する。 HTML として解釈されないため <script> もそのまま表示される。 Vue // runtime-dom/src/nodeOps.ts function createText(text) { returndocument.createTextNode(text); } function setText(n, text) { n.nodeValue = text; } nodeOps という抽象化レイヤー経由で document.createTextNode を呼 ぶ。SSR や Custom Renderer も同じ 抽象に乗る設計。 Svelte // internal/client/ function create_text(value) { returndocument.createTextNode(value); } functionset_text(text, value) { text.nodeValue = value; } コンパイラがテキスト挿入箇所を create_text / set_text の呼び出し に変換する。ランタイム本体は document.createTextNode を直接 呼ぶ薄い実装。 共通の本質 3FW とも、最終的にはブラウザの DOM API(textContent / nodeValue / createTextNode)に収束する 自動エスケープという「現象」は同じで、違うのは「どの層で実装するか」と「コードのどこに現れるか」 B-05 / 10 #fec_nagoya
  48. B-05 SSR で初期 state を HTML に埋め込む際の XSS Next.js /

    Nuxt / SvelteKit すべてに共通する SSR の典型パターン JSON 文字列のエスケープを忘れると XSS の入り口になる 問題: ユーザー入力を含む JSON を <script> タグ内にそのまま埋め込むと、 入力に紛れ込んだ </script> で <script> タグが途中で閉じられ、 その後ろが新しいスクリプトとして実行されてしまう 脆弱なコード例 // SSR:サーバーで HTML を組み立てる const userInput = '</script><script>alert(1)</script>'; // 生成される HTML const html = ` <script> window.<_STATE<_ = { name: "${userInput}" }; </script> `; 問題点: • 入力中の </script> を HTML パーサが終了と判定 • 続くコードが新しい script タグとして実行される • ブラウザは JSON の中身を理解せず HTML として処理 対策:< と > を Unicode エスケープに変換 // 対策:Unicode エスケープに変換 const safeJSON = JSON.stringify(state) .replace(/</g, '\\u003c') .replace(<>/g, '\\u003e'); // 生成される HTML consthtml = ` <script> window.<_STATE<_ = ${safeJSON}; </script> `; 安全な理由: • \u003c はタグの開始と認識されず安全に処理される • JSON.parse は \u003c を < として正しく復元できる • React, Vue, Svelte 等の主要フレームワーク共通の対策 B-06 / 10 #fec_nagoya
  49. B-06 SSR/RSC の危険地帯と主要 CVE 本編で見たクライアント側の XSS 対策は、SSR/RSC では別の層に同じ問題が現れる 2025〜2026 年に実際に公開された

    CVE を実例として並べる CVE-2025-55182 · React Server Components RCE("React2Shell") CVSS 10.0 · 2025 年 12 月 3 日公開 · 実世界での悪用観測あり React Server Components の Flight プロトコルで、信頼境界を越えたデータのデシリアライズが原因。事前認証なしでサーバーに任意コード実行が可能。 影響範囲: React 19.0〜19.2、Next.js 15.x/16.x、React Router、Waku、RedwoodSDK、Vite/Parcel の RSC プラグインなど パッチ済バージョン: React 19.0.1 / 19.1.2 / 19.2.1 Svelte SSR XSS 連発(2026 年 2 月公開) CVE-2026-27119: <option> 要素の SSR エスケープ漏れ CVE-2026-27121: スプレッド属性経由のイベントハンドラ注入 CVE-2026-27122: <svelte:element> の動的タグ名による HTML 注入 CVE-2026-27125: プロトタイプチェーン汚染による属性スプレッド いずれも Svelte 5.51.5 で修正。クライアント側レンダリングは影響なし。 CVE-2026-22029 · React Router XSS via Open Redirect 2026 年 1 月公開 · CVSS 8.0(High) React Router の Framework Mode / Data Mode / RSC モードで、loader や action からのリダイレクト URL を検証していなかった。javascript: URL を返すとクライ アントで任意 JS が実行される。 → React 本体の sanitizeURL を通らない経路で発生(ライブラリレベルでの URL 検証の重要性を示す事例) パッチ済: react-router 7.12.0 / @remix-run/router 1.23.2 本編で見た 3FW のクライアント側 XSS 対策は、ここでは無力。SSR/RSC は別の層で別の脅威に向き合う必要がある。 B-07 / 10 #fec_nagoya
  50. B-07 他の FW との比較 本編で扱った 3FW 以外の選択肢として、Angular / Solid.js /

    Qwik の XSS 対策アプローチ 設計思想の違いが、対策のアプローチにどう現れるか Angular(Google) 設計思想: フルスタック・全部入りの規範的フレー ムワーク XSS対策: 4 つのセキュリティコンテキスト (HTML / Style / URL / Resource URL)に応じ た自動サニタイズ • DomSanitizer サービスが組み込み • bypassSecurityTrust* で明示的に「信頼する」 と宣言 • Strict CSP モード(strict-csp)対応 Solid.js(Ryan Carniato) 設計思想: コンパイル時に変換する fine-grained reactivity XSS対策: JSX 経由のテキスト挿入は自動エスケー プ • テキスト挿入({value})は textContent 経由 で安全 • innerHTML プロパティは React と同じく明示的 に書く必要あり • Svelte と同様、コンパイラがテンプレートを最 適化 Qwik(Builder.io) 設計思想: resumability(再開可能性)でハイド レーションを不要に XSS対策: JSX 経由の自動エスケープ • 基本的なテキスト挿入は React と同じパターン • dangerouslySetInnerHTML 相当の API はある が警告メッセージ強め • SSR/SSG の出力は Qwik シリアライザがエス ケープ済み どの FW も「JSX/テンプレートのテキスト挿入は自動エスケープ」「明示的に opt-out する API は別途用意」という 本編 3FW と同じ基本方針。 設計思想(DI 中心 / 細粒度 reactivity / resumability)の違いは XSS 対策のアプローチには直接現れない。 B-08 / 10 #fec_nagoya
  51. B-08 Q&A: セキュリティ対策でパフォーマンスは落ちないか? 結論:一般的なユースケースではパフォーマンスへの影響は限定的 ただし、サニタイズが重くなるケースは存在する。 ボトルネックの要因は「量」「頻度」「実装」の 3 軸で整理できる。 量 (Volume)

    DOMPurify などの HTML サニタイザは内 部で DOM パースを行うため、処理時間は 入力文字列のサイズに比例する。数 KB 程 度のテキストなら無視できるが、数百 KB の HTML を一度にサニタイズすると目に見 える遅延になる。 頻度 (Frequency) キーストロークや onChange 毎にサニタイ ズを走らせると、メインスレッドをブロッ クして UI のジャンクを引き起こす。リア ルタイム検索やマークダウンエディタな ど、入力ごとに HTML 整形する場面で要注 意。 実装 (Implementation) サニタイズ結果のキャッシュ、入力のデバ ウンス、Web Worker でのオフロードな ど、実装側の工夫で負荷を抑えられる。特 に「同じ入力を何度もサニタイズしてい る」ケースは WeakMap などでメモ化する だけで効く。 実践的アプローチ:まずは実績あるサニタイザ(DOMPurify など)をそのまま導入。          パフォーマンス問題が実測で見えたときに、Web Worker / メモ化 / デバウンスを段階的に追加。 参考 DOMPurify Wiki — Security Goals & Threat Model GitHub Issue cure53/DOMPurify#577 — Running DOMPurify in a Web Worker B-09 / 10 #fec_nagoya
  52. B-09 Q&A: Next.js / Nuxt / SvelteKit ではどうなるのか? 結論:メタフレームワークでも基本となる自動エスケープの仕組みは同じ 違いは

    SSR 時のサニタイズ責任がサーバー側に移ること、 そしてメタフレームワーク固有のレイヤー(RSC、islands、ハイドレーション境界)が追加で考慮対象になること Next.js (React) React 本体の自動エスケープを継承。 追加で考慮すべき: Server Components / Server Actions の境界でのサニタイズ 動的な dangerouslySetInnerHTML の使 用箇所 2025/12 公開の CVE-2025-55182 (B-06 参照) Nuxt (Vue) Vue 本体の自動エスケープを継承。 追加で考慮すべき: v-html を含む SSR 出力は @vue/server-renderer 経由でエスケー プ useState で渡す初期値は < → \u003c 変換が走る ハイドレーション後にクライアント側の v-html へ切り替わる箇所 SvelteKit (Svelte) Svelte 本体の自動エスケープを継承。 追加で考慮すべき: サーバーから渡す data は devalue で安 全にシリアライズ 2026/02 公開の SSR XSS 群(B-06 参 照)への対策 {@html} を使う SSR コンポーネントの レビュー優先度 SSR 特有の注意点:サニタイズの責任が「ブラウザ」から「サーバー」に移る。 クライアント側だけ対策していると、SSR 出力のソースに XSS が混入する。 B-10 / 10 #fec_nagoya