何が起きるか

格納型XSSは、攻撃者のコメントなどをサイトが保存し、後から表示するときにその内容を実行可能なHTMLへ組み込み、閲覧者のブラウザで攻撃者のコードを動かしてしまう問題です。

説明用にコメント欄を考えます。攻撃者がJavaScriptを呼ぶHTML属性を含むコメントを投稿すると、サーバーの投稿処理がDBへ保存します。別の利用者が記事を開いたとき、アプリがコメントを取り出してエスケープや危険な属性の除去をせずにHTMLへ入れると、ブラウザは属性内のコードをそのサイトのページとして実行します。コードが閲覧者に見えている個人情報を読み、攻撃者のサーバーへ送れれば、その情報を攻撃者が取得できます。 たとえば存在しない画像を参照するimg要素にonerror属性が付くと、画像の読み込み失敗をきっかけに属性内のJavaScriptが動く構成があります。エスケープは、<を&lt;のような文字参照へ変え、タグを要素として作らず文字として見せる処理です。

ここでいう外部データは、アプリのプログラムの外から入るコメント、プロフィール、受信メールなどです。同じサイトの投稿フォームから送られ、サーバーがDBやメールボックスへ保存したものも含みます。内容を決めたのは投稿者や送信者なので、保存した事実は安全なHTMLとして実行してよい根拠にはなりません。

正常ならどう動くか

コメント欄を例にすると、利用者が文章を入力して「投稿」を押すと、ブラウザがサイトのサーバーへコメントの文章を送ります。サーバーの投稿処理が、受け取った文章をサイトのデータベースに保存します。後から誰かがページを開くと、アプリがデータベースからコメントを取得し、閲覧者のブラウザへ渡して表示します。

コメントを文字として表示する機能では、投稿内容にHTMLのタグが含まれていても、そのタグを文字として表示します。メールの装飾などでHTMLを許可するなら、サニタイザがメール本文のHTMLから許可しない要素・属性を除去します。保存時の検査だけで判断せず、実際の表示方法に合わせてエスケープやサニタイズを行います。

不備と攻撃が成立する流れ

攻撃者は通常のコメント投稿フォームから、JavaScriptを実行するHTMLの要素や属性を含む文章をサイトのサーバーへ送ります。サーバーの投稿処理が、その文章をデータベースに保存します。攻撃者がデータベースへ直接接続して書き込む必要はありません。

別の利用者がコメントのあるページを開くと、アプリがデータベースから保存済みのコメントを取得します。表示処理が実行可能な要素や属性を除去せず、文字としてのエスケープもせずに、そのコメントをページのHTMLへ組み込むと問題になります。ブラウザがコメント内の要素や属性をHTMLとして解釈し、含まれたJavaScriptを閲覧者のブラウザで実行してしまいます。この、投稿内容のコードが閲覧者のページで実行される部分がXSSです。

オリジンはURLのスキーム・ホスト・ポートの組です。コードは投稿者が誰かではなく、実行されるページのオリジンで動きます。そのため閲覧中のサイトの通常のJavaScriptと同じように、ページの要素や表示内容・入力値を表すDOMを読み書きし、同じオリジンのAPIへ接続して応答を読める場合があります。攻撃者はコードへ情報の読み取りや外部送信、設定変更の処理を含めます。

格納型では、攻撃者の投稿をサイトが保存しているため、他の利用者が普段どおりコメントを開く操作でコードが実行され得ます。反射型のように、攻撃用の入力を含むURLなどを利用者に送信させる操作へ、その都度誘導する必要はありません。

起こり得る被害と条件

以下は、コメント欄と非公開メッセージ機能があり、Cookieでログイン状態を管理するサイトでの説明用の例です。攻撃者は、これから説明する情報の取得・送信や画面の変更を行うコードをコメントに含めて投稿します。被害者がログイン中にそのコメントを開き、表示処理の不備でコードが実行されることを前提にします。

Cookieの値をコードが読まなくてもAPIを操作できる理由は、通常のログイン処理と同じです。この例のサイトでは、ログイン成功時に、サーバーがセッションIDと利用者を対応付けて保存します。サーバーはログイン処理のHTTPレスポンスのSet-Cookieヘッダーで、そのIDを含むCookieをブラウザへ保存するよう指示します。

その後、ページのJavaScriptが同じオリジンのAPIへfetchでHTTPリクエストを送ります。fetchの既定の設定では、ブラウザが送信条件に合うCookieを、そのHTTPリクエストのCookieヘッダーに自動で付けます。サーバーは、受け取ったCookieのセッションIDから利用者を特定します。攻撃者のコードも同じページのJavaScriptとして動くため、被害者本人に許可されたAPIの応答を読んだり、設定変更を要求したりできる場合があります。

HttpOnlyは、JavaScriptからそのCookieの値を読み取ることを防ぐ属性です。HttpOnlyを設定しても、ブラウザは送信条件に合うCookieを、APIへのHTTPリクエストのCookieヘッダーに付けます。そのため、Cookieの値自体の窃取を防げても、XSSのコードによる被害者のログイン状態を使ったAPI操作までは防げません。

この説明でブラウザが送るのは、セッションそのものではなく、セッションIDを含むCookieです。ログイン済みであることだけで全ての操作が許されるわけではありません。サーバー側の認可や重要操作の再認証などによって、実際に読める情報や変更できる設定は制限されます。

情報を読む例で攻撃者が狙うのは、自分のアカウントでは読めない、被害者の非公開メッセージの本文です。コメントから実行されたコードが、同じオリジンにあるメッセージ取得APIへHTTPリクエストを送ります。その際、ブラウザは、送信条件に合う被害者のログイン用Cookieを、そのHTTPリクエストのCookieヘッダーに付けて送信します。

サイトのサーバーは、受け取ったCookie内のセッションIDを照合し、被害者本人からのリクエストとして処理します。APIが被害者のメッセージ本文をHTTPレスポンスで返すと、被害者のブラウザで動いている攻撃者のコードが、そのレスポンスから本文を読み取ります。

さらに、そのコードが読み取った本文を含むHTTPリクエストを攻撃者の管理するサーバーへ送信すると、攻撃者はそのサーバーで本文を入手できます。これで、自分には閲覧権限がない他人の会話内容を盗めます。メッセージに個人情報や機密情報が含まれていれば、それらも漏えいします。攻撃者が情報を入手するには、APIの応答を読めることに加え、CSPなどに妨げられず攻撃者のサーバーへ情報を送信できることが必要です。

APIを使わず、表示中のページにある個人情報をコードがDOMから読み取る場合もあります。その情報を含むHTTPリクエストが攻撃者のサーバーへ届けば、攻撃者は画面に表示されていた個人情報を入手できます。

設定を変える例では、コメントから実行されたコードが、登録メールアドレスを変更するAPIへHTTPリクエストを送ります。コードはそのリクエストに、変更先として攻撃者のメールアドレスを含めます。ブラウザがそのHTTPリクエストのCookieヘッダーに被害者のログイン用Cookieを付け、サーバーが変更を許可すると、被害者のアカウントの登録メールアドレスが更新されます。

変更後のアドレスにサイトが通知を送るなら、攻撃者が被害者宛ての通知を受け取ることになります。そのアドレスでパスワード再設定も受け付けるサイトなら、攻撃者が再設定メールを受け取るおそれがあります。再設定メールのリンクから新しいパスワードを設定できる条件も満たせば、攻撃者はそのパスワードで被害者のアカウントへログインできます。メールアドレスの変更に現在のパスワードや変更先・旧アドレスでの確認を求める場合などは、その確認も満たさなければ変更できません。

偽の画面を出す例では、コメントから実行されたコードが、閲覧中のページのDOMを書き換えます。「セッションが切れました。再ログインしてください」という文と、ID・パスワードの入力欄を表示して、被害者に入力を促します。ページ内の表示を変えるため、アドレスバーが正しいサイトのURLのままでも起こり得ます。

被害者が偽の入力欄にID・パスワードを入力すると、そのページで動く攻撃者のコードが入力値を読み取ります。コードが入力値を含むHTTPリクエストを攻撃者のサーバーへ送信できれば、攻撃者は被害者のID・パスワードを入手します。画面が表示されただけで入力値を盗めるわけではなく、被害者が入力し、入力値の読み取りと外部への送信が成立することが条件です。

防ぎ方と、防げる理由

コメントを文字として表示する場合は、表示先にdivなどの要素を用意し、その要素のtextContentへコメントの文字列を代入します。textContentは文字列をHTMLとして解析しないため、コメントにタグやイベント属性が含まれていてもコードとして実行しません。サーバー側でHTMLを生成する場合は、テンプレートの標準エスケープを使い、挿入する場所に合う方法でコメントを出力します。

メール本文などでHTMLの装飾を許可する場合は、保守されたサニタイザで、許可しない要素や属性を本文のHTMLから除去してから表示します。リンクのURLや属性の値も検証します。サニタイズ済みのHTMLに未検査の投稿内容を連結し直すと、実行可能なタグやイベント属性が再び含まれるため、後の加工も確認します。

CSPは、読み込めるスクリプトや通信先などを制限する追加策として使います。設定によっては、コードの実行や、読み取った情報の攻撃者サーバーへの送信を制限できます。ただし、表示処理の不備そのものを修正するものではないため、エスケープやサニタイズと組み合わせます。

説明用の擬似コード(実際の製品コードではありません)

// 危険: 保存済みの値でもHTMLとして解釈される
preview.innerHTML = savedComment;
// 修正: コメントを文字として表示する要件の場合
preview.textContent = savedComment;

よくある誤解

CSRFトークンは、別サイトから利用者の意図しないリクエストを送らせる攻撃への対策です。XSSのコードは、閲覧中のページのDOMや同じオリジンのAPIの応答から、そこに含まれるCSRFトークンを読み取れる場合があります。コードがその値を設定変更のリクエストへ含めれば、サーバーのトークン検査を通過するおそれがあります。

Reactでも、保存した投稿のHTMLをdangerouslySetInnerHTMLなどへ渡す場合は、サニタイズで実行可能な要素や属性を除く必要があります。格納型・反射型は攻撃用の入力が保存されるかどうか、DOMベースはブラウザ内の処理でXSSが発生するかどうかという分類なので、格納型とDOMベースの両方に当てはまる場合もあります。

実装の確認点

投稿フォームからサーバーへ届く値と、データベースへ保存される内容を確認します。対象は、投稿を表示するプレビュー、通知、通常のページ、管理画面です。それぞれの表示処理がどの値を受け取り、textContentやinnerHTMLなどのどの処理へ渡すかを確認します。

保存時の検査だけで安全と判断せず、各表示処理がコメントを文字として扱うか、許可したHTMLだけを表示するかを確認します。サニタイズ後のHTMLへ未検査の値を連結していないかも確認します。被害の評価では、閲覧者の権限で呼べるAPIと、そのAPIから取得できる応答を調べます。設定変更に追加の確認が必要か、読み取った情報を外部へ送信できるかも調べます。

出典・確認範囲

  1. [1] Web Security Academy: 格納型XSSPortSwigger / 確認日 2026-11-01
  2. [2] Cross Site Scripting (XSS)OWASP Foundation / 確認日 2026-10-29
  3. [3] Set-Cookie header: HttpOnlyMDN Web Docs / 確認日 2026-10-29
  4. [4] Using the Fetch API: Including credentialsMDN Web Docs / 確認日 2026-10-29
  5. [5] Exploiting cross-site scripting vulnerabilitiesPortSwigger / 確認日 2026-10-29
  6. [6] Cross Site Scripting Prevention Cheat SheetOWASP Foundation / 確認日 2026-10-29