JSON Toolkit

JSON 経由で XSS はどう起きるか

要点:JSON は決してそれ自体では実行されません。XSS はそれを HTML として扱った瞬間に起きます。サーバー側の文字列連結、フロントエンドの innerHTML、JSON を script タグに直接インライン化する場合です。

中級約 4 分で読めます2026-10-30 公開2026-10-30 更新
JSON が XSS になる三つの経路と防止方法

要点:JSON はそれ自体では決して実行されません。まず三つの出力経路を閉じましょう。サーバー側は文脈でエスケープし、innerHTML ではなく textContent を使い、JSON をインライン化するときは < を \u003c に置き換えます。

{"name": "<img src=x onerror=alert(1)>"} は正しいデータです。危険かどうかは、完全にどこに置くかで決まります。

入口その一:サーバー側の文字列構築

res.send(`<div>Hello, ${data.name}</div>`);

ブラウザは data.name 内のタグをパースします。ここで JSON は単なる容器で、過失は誤った文脈に対するエスケープです。

修正:テンプレートエンジンの既定エスケープ(EJS の <%= %>、Vue と Django の {{ }})を使います。ユーザー制御のものには <%- %>、v-html、|safe を避けます。

入口その二:innerHTML

el.innerHTML = user.bio;    // マークアップをパースする
el.textContent = user.bio;  // テキストを挿入する

React の {user.bio} と Vue の {{ user.bio }} はテキストノードを作り、既定で安全です。dangerouslySetInnerHTML と v-html は明示的なオプトアウトです。

リッチテキストが本当に必要なときは、バックエンドを信頼するのではなく許可リストでサニタイズします(DOMPurify など)。

入口その三:script タグへのインライン化

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

データ内の </script> がブロックを早く閉じ、その後のすべてが新しいスクリプトとして実行されます。最も見落とされるケースです。

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

依然として正しい JSON で、パース後は同一、タグを閉じることはもうできません。Next.js と Nuxt は __NEXT_DATA__ と __NUXT__ に対して同じことをします。

知っておくべき隣人二つ

JSON ハイジャック:古いブラウザは <script src> 経由で読み込んだ JSON を実行し、Array を再定義すると内容が漏れました。現代のブラウザは修正済みで、Content-Type: application/json と X-Content-Type-Options: nosniff が残りを閉じます。

JSONP の残骸:ユーザー制御のコールバック名は、設計上任意のコード実行です。新しいシステムでは使わないでください。

チェックリスト

  • JSON フィールドが HTML に届くすべての場所で、その文脈(HTML、属性、JS、URL)に応じてエスケープされている
  • API の文字列を消費する innerHTML、v-html、dangerouslySetInnerHTML がない
  • インライン化した JSON が <、>、& をエスケープする
  • リッチテキストはブラックリストではなく許可リストのサニタイズを通る
  • レスポンスが Content-Type: application/json と nosniff を運ぶ

FAQ

正しい JSON が依然として XSS を引き起こせるのはなぜですか?

JSON はただのテキストで決して実行されず、正しいことはパースできることだけを意味します。危険は HTML やスクリプトの文脈に書き込まれたときに現れます。ブラウザがタグをパースするか、</script> がブロックを早く閉じて残りがコードとして実行されます。過失はデータ形式ではなく、出力時の文脈を考慮したエスケープの欠如です。

API フィールドの描画に textContent と innerHTML のどちらを使うべきですか?

textContent を優先します。値をプレーンテキストとして挿入するので、タグは文字通り表示されます。innerHTML はマークアップをパースするので、<img onerror=...> を含む API フィールドが実行されます。React の {user.bio} と Vue の {{ user.bio }} は既定でテキストノードを作り、dangerouslySetInnerHTML と v-html は明示的なオプトアウトです。

<script> タグにインライン化する JSON はどうエスケープすべきですか?

はい。データ内の </script> がブロックを早く閉じ、その後のすべてが新しいスクリプトとして実行されます。シリアライズ後に <、>、& を \u003c、\u003e、\u0026 に置き換えます。結果は依然として正しい JSON で、同一にパースされ、タグを閉じることはもうできません。

さらに読む