XSS(クロスサイトスクリプティング)とCSRF(クロスサイトリクエストフォージェリ)の違いは、攻撃コードがどこで動くかです。XSSは攻撃者のスクリプトを標的サイトのページの中で実行させ、利用者の画面・Cookie・入力内容に手を出します。CSRFは別サイトから標的サイトへリクエストを送らせ、ログイン中の利用者の権限で処理を実行させます。CSRFの攻撃者は送るだけで、返ってきた応答は読めません。この記事では、両者の違いを比較表で示したうえで、文脈ごとのエスケープ、CSRFトークン、Sec-Fetch-Siteヘッダー、SameSite Cookieの既定値の実態を、Go 1.27.2とPHP 8.5.8で動かした結果とあわせて整理します。
まとめ
- XSSは、出力のエスケープ漏れを突いて、標的サイトのページ内で攻撃者のスクリプトを動かす攻撃です。同じページの中で動くので、画面の内容もCSRFトークンも読めます。
- CSRFは、別サイトから標的サイトへリクエストを送らせ、ブラウザが自動で付けるCookieを使って処理を実行させる攻撃です。攻撃者は応答を読めず、できるのは送ることだけです。
- XSSの根本対策は、出力する場所(HTML本文・属性値・URL・script内)ごとのエスケープです。CSP・Trusted Types・HttpOnlyは被害を小さくする層です。
- CSRFの根本対策は、フレームワーク標準のCSRFトークンです。新しい選択肢として
Sec-Fetch-Siteヘッダーで別サイトからの送信を弾く方法があり、Go 1.25以降では標準ライブラリの機能として提供され、Laravel 13では標準のwebミドルウェアに組み込まれています。SameSite=Laxを既定で適用するのはChrome系だけで、FirefoxとSafariは既定でLaxにしていません。 - XSSが残っていると、CSRFトークン・Sec-Fetch-Site・SameSiteの3つがまとめて素通りになります。塞ぐ順番はXSSが先です。
XSSとCSRFの比較:成立条件・被害・対策
実装上の違いが出る観点に絞って並べます。分類はOWASP Top 10:2025と2025 CWE Top 25によります。
| 観点 | XSS | CSRF |
|---|---|---|
| 突かれる欠陥 | 出力のエスケープ漏れ | 送信元の確認漏れ |
| 攻撃コードが動く場所 | 標的サイトのページ内 | 攻撃者のサイト |
| 攻撃者が応答を読めるか | 読める | 読めない |
| 被害者側の条件 | 細工URLを開く・該当ページを見る | 標的サイトのCookieを持っている |
| 主な被害 | 情報窃取・画面改ざん・なりすまし操作 | 送金・退会・設定変更 |
| 根本対策 | 出力文脈ごとのエスケープ | CSRFトークン・送信元の検証 |
| OWASP Top 10:2025 | A05 Injection | A01 Broken Access Control |
| CWE Top 25(2025) | 1位(CWE-79) | 3位(CWE-352) |
CWE Top 25はスコアにも差があり、CWE-79が60.38、CWE-352が13.64です。米CISAの「悪用が確認された脆弱性」カタログ(KEV)に載ったCVEの数は、CWE-79が7件、CWE-352が0件でした。この集計では、KEVに掲載されたCVE数はXSSのほうが多くなっています。ただし、これは攻撃や被害の発生件数を比較した統計ではありません。
見分け方は「攻撃者が応答を読めるか」
反射型XSSとCSRFは、どちらも「罠のURLやページを開かせる」ところから始まるため混同されます。区別するには、攻撃者が標的サイトの応答を読めるかを見ます。ブラウザの同一オリジンポリシーは、別オリジンのページが標的サイトの応答を読むことを禁じています。CSRFの攻撃者は別サイトにいるので、フォームを送らせることはできても、結果の画面やトークンは見えません。XSSのスクリプトは標的サイトのページの中で動くので、この制限を受けません。
同じ理由で、CORSの設定だけでは、通常のフォーム送信によるCSRFを防げません。ただしAPIでカスタムヘッダーを必須にし、CORSで許可するオリジンを厳密に限定する設計は、CSRF対策として利用できます。<form method="post">による通常のフォーム送信はプリフライトなしで届き、サーバーで処理されます。
ログインしていない利用者にも起きる「ログインCSRF」
比較表の「Cookieを持っている」には例外があります。ログインCSRFでは、攻撃者が自分のIDとパスワードで標的サイトのログインフォームを被害者のブラウザから送らせます。被害者は気づかないまま攻撃者のアカウントで操作し、入力したカード情報や検索履歴が攻撃者側に残ります。OWASPのCSRF対策チートシートは、ログイン前にもセッションを作ってログインフォームにトークンを入れること、ログイン後はそのセッションを破棄して作り直すことを求めています。
XSSの仕組みと3つの種類
XSSは、利用者が入力した値や外部から受け取った値を、Webアプリがエスケープせずにページへ出力したときに起きます。<script>やonerror=を含む文字列がHTMLとして解釈されると、そのスクリプトは標的サイトの権限で動きます。代表的な呼び方は反射型・格納型・DOM型です。ただし、反射型と格納型はデータの保存・反映経路、DOM型はブラウザ側の処理に着目した分類で、互いに重なる場合があります。
| 種類 | 攻撃文字列の経路 | 発火する相手 | サーバー側の痕跡 |
|---|---|---|---|
| 反射型 | URLやフォームの値がそのまま応答へ | 細工URLを開いた人 | 応答に残る |
| 格納型(蓄積型) | DBに保存され、表示のたびに出力 | そのページを見た全員 | DBに残る |
| DOM型 | ブラウザのJSがURL等をDOMへ書き込む | 細工URLを開いた人 | 残らないことがある |
反射型XSS:検索語やエラーメッセージの表示箇所
リクエストに含まれる値をそのまま応答に書き出す箇所で起きます。OWASPの分類は、典型としてエラーメッセージと検索結果を挙げています。攻撃者はスクリプトを埋め込んだURLをメールやSNSで配ったり、罠のページからPOSTさせたりして、相手のブラウザで実行させます。細工したリクエストで管理者にスクリプトを実行させると、その管理者の権限で操作されるおそれがあります。
格納型XSS:投稿・プロフィール・管理画面の一覧
OWASPは格納型XSSの保存先として、データベース、掲示板、訪問ログ、コメント欄などを挙げています。投稿、レビュー、プロフィール、問い合わせ内容も、保存後の表示処理で確認する対象です。一度仕込まれると、そのページを見た全員が対象になります。見落としやすいのは管理画面です。利用者が送った問い合わせを管理者向けの一覧で表示する箇所にエスケープ漏れがあると、権限の強い管理者のブラウザで発火します。
DOM型XSS:innerHTMLなどへの書き込み
サーバーの応答は安全でも、ブラウザ側のJavaScriptがlocation.hashやURLSearchParamsから読んだ値をinnerHTML、document.write、evalへ渡すと起きます。URLの#以降はサーバーへ送られないため、サーバーのログやWAFでは見えません。サーバー側のエスケープでは防げないので、書き込み先をtextContentに変えるか、後述のTrusted Typesで危険な代入を止めます。
CSRFの仕組みと成立する条件
CSRFは、ブラウザが標的サイト宛てのリクエストにそのサイトのCookieを自動で付ける動作を利用します。攻撃者のページに次のようなフォームを置いておき、開いた瞬間に自動送信させます。
<!-- 攻撃者のページ(evil.example)に置かれる例 -->
<form action="https://bank.example/transfer" method="post">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="100000">
</form>
<script>document.forms[0].submit();</script>
標的サイトのセッションCookieが付いたリクエストが届けば、サーバーは本人の操作と区別できません。成立には次の条件がそろう必要があり、どれか1つを崩せば防げます。
- 認証をCookie(またはBasic認証など、ブラウザが自動で付ける情報)だけで判断している
- 送金先や金額など、処理に必要なパラメータを攻撃者が事前に用意できる(推測できない値を要求していない)
- サーバーが、リクエストが自サイトの画面から送られたかを確かめていない
反対に、APIがAuthorization: Bearerヘッダーでトークンを受け取り、Cookieを認証に使っていない構成では、ブラウザがトークンを自動で付けないためCSRFは成立しません。この形の詳細はBearer Tokenの解説にまとめています。ただし、同じAPIがCookieでも認証を受け付けるなら対策が必要です。
XSS対策:出力文脈別のエスケープと補助防御
IPA「安全なウェブサイトの作り方」1.5は、XSSの根本的解決として、出力要素のエスケープ(5-(i))、URLをhttp://・https://で始まるものに限ること(5-(ii))、<script>の中身を動的に生成しないこと(5-(iii))などを挙げています。入力値のチェック(5-(v))は保険的対策の扱いです。入力時の検証では防ぎきれないので、守りの中心は出力時に置きます。
HTML本文・属性値のエスケープとURLスキームの検証
PHPのhtmlspecialcharsは、PHP 8.1以降は引数を省略しても一重引用符までエスケープします。それでも防げない文脈があります。PHP 8.5.8で実行した結果は次のとおりです。
| 入力 | 既定の引数 | ENT_NOQUOTES指定 |
|---|---|---|
<script>… |
無害化される | 無害化される |
" onmouseover="… |
無害化される | 属性を抜けられる |
javascript:alert(1) |
そのまま出力 | そのまま出力 |
javascript:には<も引用符も含まれないため、hrefに入れるとクリック時にスクリプトが動きます。URLを出力するときは、エスケープとは別にスキームを確かめます。
<?php
// HTML本文・引用符で囲んだ属性値:エスケープ
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// URL:http と https 以外のスキームを拒否してからエスケープ
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true)) {
$url = '#';
}
echo '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">';
属性値は必ず引用符で囲みます。<input value=<?= $v ?>>のように引用符が無いと、空白1つで別の属性を足されます。IPA 5-(iii)のとおり<script>の中身は動的に組み立てないようにし、JavaScriptへ値を渡したいときはdata-属性などに出力してJavaScript側で読みます。
フレームワーク別の自動エスケープ解除構文
主要なテンプレートエンジンは既定で出力をエスケープします。自動エスケープを明示的に外した箇所は、XSSの重点確認対象です。ただし、URLの検証漏れや危険なDOM操作など、エスケープ解除以外の経路でも発生します。コードレビューやgrepでは、次の書き方を探します。
| 環境 | エスケープを外す書き方 |
|---|---|
| React | dangerouslySetInnerHTML |
| Vue | v-html |
| Laravel Blade | {!! $value !!} |
| Django テンプレート | |safe・mark_safe() |
| Rails ERB | raw・html_safe・<%== %> |
利用者にHTMLの入力を認める(リッチテキストエディタなど)場合は、エスケープではなくサニタイザで、許可したタグと属性だけを残します(IPA 5-(vi))。サニタイザ自体にも脆弱性が出るため、版の管理が必要です。DOMPurifyの例はDOMPurify 3.4.17で直った脆弱性と点検手順で扱っています。React・Vueでも、hrefに利用者の値を入れる箇所では上のスキーム確認が必要です。
CSP・Trusted Types・HttpOnlyは被害を減らす層
Content-Security-Policy(CSP)をscript-src 'nonce-…'のように設定すると、エスケープ漏れがあっても、nonceを持たないインラインスクリプトは実行されません。設定方法はCSPのnonce方式の解説にまとめています。DOM型XSSには、require-trusted-types-for 'script'でinnerHTMLなどへの文字列代入をブラウザに拒否させるTrusted Typesが効きます。Firefox 148とSafari 26が対応し、Chrome 83以降と合わせて主要ブラウザで使えるようになりました。
CookieのHttpOnly属性は、document.cookieからセッションIDを読み出されるのを防ぎます。止められるのはセッションIDの持ち出しだけです(理由は後述のよくある質問を参照)。
CSRF対策:トークン・Sec-Fetch-Site・SameSiteの組み合わせ
IPA「安全なウェブサイトの作り方」1.6の根本的解決は、POSTで送らせてhiddenパラメータの秘密情報を照合する方法(6-(i)-a)、パスワードの再入力(6-(i)-b)、Refererによるリンク元の確認(6-(i)-c)です。OWASPのチートシートは、これにFetch Metadata(Sec-Fetch-Site)を加えています。
CSRFトークン:フレームワーク標準の保護と除外設定
OWASPが最初に挙げるのは「フレームワークに組み込みのCSRF保護があればそれを使う」ことです。Rails 8.1はActionController::Baseを継承したコントローラーで、Django 6.1はCsrfViewMiddlewareで、Laravel 13はwebミドルウェアグループで、それぞれ既定で有効になっています。自前で作る場合、セッションを持つ構成では、セッションに保存したトークンと送信された値を照合するSynchronizer Token方式を使います。セッションを持たない構成なら、HMACでセッションに結び付けたSigned Double-Submit Cookie方式です。トークンの生成と送り方はCSRFトークンの仕組みとSPAでの実装、検証で弾かれたときの切り分けはInvalid CSRF tokenの直し方で扱っています。
よくある失敗は、APIのルートをCSRF保護の除外リストに入れたまま、そのAPIをCookieセッションで認証しているケースです。除外は、署名を検証するWebhookなど、ブラウザが自動送信する認証情報に依存せず、別の仕組みで要求の正当性を確認できるエンドポイントに限定します。
Sec-Fetch-Siteによる送信元の検証と拒否条件
ブラウザは、リクエストがどこから出たかをSec-Fetch-Siteヘッダーで知らせます。値はsame-origin・same-site・cross-site・none(アドレスバー入力やブックマーク)の4つで、Chrome 76・Firefox 90・Safari 16.4以降が送ります。JavaScriptから書き換えられないため、トークンを使わずに別サイトからの送信を見分けられます。
Go 1.25で標準ライブラリに入ったhttp.CrossOriginProtectionは、この方式を実装しています。Go 1.27.2で、https://shop.example.com宛てのリクエストにヘッダーを付け替えて送った結果は次のとおりです(https://admin.example.comをAddTrustedOriginで許可済み)。
cop := http.NewCrossOriginProtection()
if err := cop.AddTrustedOrigin("https://admin.example.com"); err != nil {
panic(err)
}
handler := cop.Handler(mux)
| リクエスト | 結果 |
|---|---|
| POST・Sec-Fetch-Site: same-origin | 200 |
| POST・Sec-Fetch-Site: same-site | 403 |
| POST・Sec-Fetch-Site: cross-site | 403 |
| POST・Sec-Fetch-Site: none | 200 |
| GET・Sec-Fetch-Site: cross-site | 200 |
| POST・ヘッダーなし(curl等) | 200 |
| POST・Originのみ(他ドメイン) | 403 |
| POST・cross-site+許可済みOrigin | 200 |
same-site(同じ登録ドメインの別サブドメイン)からのPOSTも拒否されます。GETは無条件で通るので、GETで状態を変える処理には効きません。Sec-Fetch-SiteもOriginも無いリクエストは「ブラウザ以外」とみなして通します。
Laravel 13のPreventRequestForgeryは、Sec-Fetch-Site: same-originならトークン照合を省いて通し、それ以外はCSRFトークンの照合に回します。same-siteを通すにはallowSameSite()の明示が必要です。OWASPは、このヘッダーを送らない古いブラウザに備えてOrigin・Refererによる検証を併せて置くこと、ヘッダーはHTTPS(とlocalhost)でしか送られないことを挙げています。
SameSite Cookieの既定値:Chrome・Firefox・Safariの比較
SameSite属性は、別サイトからのリクエストにCookieを付けるかをブラウザに指示します。Strictは一切付けず、LaxはクロスサイトではGETなど安全なメソッドによるトップレベル遷移に限って送信を許し、NoneはSameSiteによる制限を設けません(Secure必須)。注意が必要なのは、属性を書かなかったときの扱いがブラウザごとに違うことです。
| ブラウザ | SameSite省略時 | 根拠 |
|---|---|---|
| Chrome・Edge | Laxとして扱う(Chrome 80〜) | Chromium FAQ・MDN |
| Firefox | 既定ではLaxにしない | 設定値laxByDefault=false |
| Safari | 既定ではLaxにしない | MDN互換表で未対応 |
Firefoxはnetwork.cookie.sameSite.laxByDefaultという設定でLax既定を試しましたが、リリース版の既定値はfalseです。「Firefox 96でLaxが既定になった」とする解説がありますが、MDNの互換データでもフラグ付きの対応として登録されています。さらにChromeには、SameSiteを省略したCookieは発行から2分以内ならトップレベルのクロスサイトPOSTにも付けるという例外があります(Chromium「SameSite Updates」)。SameSiteを書かずに発行したセッションCookieなら、ログイン直後の2分間は別サイトからのPOSTにもCookieが付きます。
ブラウザの既定に任せず、セッションCookieにはSameSite=LaxかStrictを明示します。それでも、SameSiteが見るのはオリジンではなくサイト(登録ドメイン。Chrome 91以降はスキームも含めて比較)です。user-content.example.comのように利用者のコンテンツを置くサブドメインや、乗っ取られたサブドメインからのリクエストは「同じサイト」として扱われ、Cookieが付きます。OWASPがSameSiteを多層防御の一層と位置づけ、トークンの代わりにしないとしているのはこのためです。
Set-Cookie: session=…; Path=/; Secure; HttpOnly; SameSite=Lax
XSSがCSRF対策を無効にする理由と塞ぐ順番
OWASPのCSRFチートシートは冒頭で「XSSはすべてのCSRF対策を破れる」と注意しています。XSSのスクリプトは標的サイトのページ内で動くため、各対策から見ると正規の操作と区別がつきません。
- CSRFトークン:ページ内のhiddenフィールドや
metaタグからスクリプトがそのまま読み出せる - Sec-Fetch-Site:同じページからの
fetchなのでsame-originが付く - SameSite:同じサイトからのリクエストなのでCookieが付く
このため、既存のアプリを点検するときは次の順に進めます。
- XSS:テンプレートでエスケープを外している箇所(前述の表)と、
innerHTMLなどへの代入を洗い出して直す - GETで状態を変える処理:退会・削除・設定変更がGETで動く箇所をPOST等に変える。SameSite=LaxもGoのCrossOriginProtectionもGETは通す
- CSRF保護の有効化:フレームワーク標準の保護が全ルートで有効か、除外リストにCookie認証のAPIが入っていないかを確かめる
- Cookie属性:セッションCookieに
Secure・HttpOnly・SameSiteを明示する
副作用の無いGETだけの公開ページでは、通常CSRFトークンは不要です。APIについては、Cookieの有無だけでなく、Basic認証などブラウザが自動送信する認証情報を使っていないかも確認します。CSRF保護は、ブラウザが自動送信する認証情報で実行できる状態変更操作に適用し、ログインフォームも保護対象に含めます。自社だけで点検しきれない場合は、第三者による脆弱性診断・セキュリティ診断で、XSSとCSRFを含む診断項目を通して確認する方法もあります。診断項目の全体像はWebアプリケーション脆弱性診断の項目にまとめています。手元で攻撃を再現して学ぶなら、XSSとCSRFの演習モジュールを持つDVWAが使えます。
よくある質問
XSSとCSRFはどちらが危険ですか?
攻撃者ができることの範囲はXSSのほうが広いです。XSSは画面の内容を読んだうえで本人として操作でき、CSRFは送れるリクエストの範囲に限られます。XSSはCSRF対策も無効にするので、対処はXSSを先にします。
HttpOnlyを付ければXSSは防げますか?
防げません。HttpOnlyはJavaScriptからCookieを読まれるのを防ぐだけです。XSSのスクリプトは被害者のブラウザの中から標的サイトへリクエストを送れるので、Cookieを盗まなくても本人として操作できます。根本対策は出力時のエスケープです。
SameSite=Laxを設定すればCSRFトークンは不要ですか?
不要にはなりません。LaxはトップレベルのGET遷移ではCookieを送り、同じ登録ドメインの別サブドメインからのリクエストも防げません。フレームワーク標準のCSRFトークンを主にして、SameSiteは追加の層として使います。
CSRFとSSRFの違いは何ですか?
リクエストを送らされる主体が違います。CSRFは利用者のブラウザが、利用者のCookieを付けて標的サイトへ送らされます。SSRFはサーバー自身が、攻撃者の指定したURLへ送らされます。SSRFではクラウドのメタデータなど内部ネットワークが狙われます。仕組みと防御はSSRFの解説で扱っています。
Synchronizer Token方式は、サーバー側で保持する秘密値と送信されたトークンを照合します。hiddenはトークンの送信手段であり、Double Submit Cookie方式でも使えます。Cookieにも入れる方式(Double Submit Cookie)は、Cookieの値とリクエストの値の一致を見るため、セッションを持たない構成で使います。OWASPは、署名の無い素朴なDouble Submitを非推奨とし、HMACでセッションに結び付けた署名付きの方式を推奨しています。