ゆめさくセキュリティ

レッスン 2-3

クロスサイトリクエストフォージェリ

ログイン済みのブラウザは、攻め手の指示にも従います。

ブラウザはクッキーを自動で付けて送るので、別のサイトから送らされた要求も、本人の要求と見分けがつきません。状態を変える操作を GET で受ける脆いコードと、POST だけを受け、Origin とトークンを確かめる直したコードを並べ、SameSite の効く範囲と、XSS との被害の出方の違いまでを扱います。

  • 目安60 分
  • 出典を確かめた日2026年10月24日
  • 修了の条件XSS と CSRF の被害の出方の違いを説明できる

なぜ通るのか

会員がメールアドレスを変える画面を作ったとします。ログインすると、サーバーはセッションの ID をクッキーに入れて返し、以後の要求ではそのクッキーで本人を見分けます。

ここで、サーバーが「クッキーが付いていれば本人の操作」とだけ判断していると、穴が開きます。クッキーを付けるのはブラウザで、どのページから出た要求でも、行き先のサイトのクッキーを付けて送るからです。

クロスサイトリクエストフォージェリ (CSRF) は、利用者が別のサイトを開いている間に、ブラウザに本人の権限で要求を送らせる穴です。OWASP の Cheat Sheet は、守られていないサイトでは、本人が送った要求と送らされた要求を区別できないと説明しています。

本人の要求と送らされた要求が混ざる所

サーバーから見ると、届く要求は同じ形です。同じクッキー、同じ項目、同じ値が付いています。違うのは「どのページから、本人の意思で出たか」だけで、クッキーはそれを運びません。

悪意が無くても、似たことは起きます。たとえば、ほかのサイトに置かれた画像やリンクが、たまたま自分のサイトの「設定を変える」住所を指していたとします。その住所が GET で状態を変えるなら、利用者がそのページを開くだけで設定が変わります。

3 つの守り

守りは、要求の出どころを確かめる仕組みを重ねて作ります。

守り何を見るか
トークン自分のページに埋めた、推測できない値が返ってきたか
Origin の確かめブラウザが付ける Origin ヘッダーが自分のサイトか
SameSiteクッキーを別のサイトからの要求に付けるか

トークンと Origin の確かめはサーバーで行う守りで、SameSite はクッキーにかける印です。OWASP は、状態を持つ作りならトークン (同期トークンの型) を使い、SameSite は重ねて使う守りだとしています。

XSS との違い

前のレッスンのクロスサイトスクリプティング (XSS) と、名前がよく似ています。被害の出方は、次のように違います。

XSSCSRF
何が起きるか攻め手のスクリプトがページの中で動くブラウザが本人の権限で要求を送らされる
攻め手に見える物ページの中身と応答を読める応答は読めない。送らせるだけ
止める所出力のエスケープ要求が本人の操作かの確かめ

CSRF で攻め手ができるのは、決まった操作を 1 回送らせることまでです。XSS では、スクリプトがページの中にいるので、トークンも画面も読めます。OWASP は、XSS があれば CSRF の対策はすべて越えられると注意しています。CSRF を直しても、XSS は別に直す必要があります。

脆いコードと直したコード

ラボの中の小さな関数で比べます。要求は、方式 (method)、項目、クッキー、ヘッダーを持つ小さなオブジェクトで表します。Python の標準の部品だけで動き、外とは通信しません。サイトの住所は 127.0.0.1:8000 の想定です。

GET で受けるか、POST だけを受けるか

脆いコードどの方式でも変更する
def change_email(req):sid = req.cookies.get("sid")user = SESSIONS.get(sid)if user is None:return 401user["email"] = req.params["email"]return 200
直したコードPOST のときだけ変更する
def change_email(req):sid = req.cookies.get("sid")user = SESSIONS.get(sid)if user is None:return 401if req.method != "POST":return 405user["email"] = req.params["email"]return 200

脆い方は、GET の要求でもメールアドレスを書き換えます。GET は、リンクを開く、画像を読み込むといった、ふだんの閲覧で送られる方式です。状態を変える操作を GET で受けると、別のサイトのページを開いただけで変更が起きます。IPA も、処理を実行するページは POST で受けるよう書いています。

ただし、POST にしただけでは止まりません。別のサイトのフォームからも POST は送れるからです。次の比べで、出どころを確かめます。

クッキーだけで本人とみなすか、出どころも見るか

脆いコードクッキーがあれば本人とみなす
def change_email(req):sid = req.cookies.get("sid")user = SESSIONS.get(sid)if user is None:return 401if req.method != "POST":return 405user["email"] = req.params["email"]return 200
直したコードOrigin とトークンを確かめる
SITE = "http://127.0.0.1:8000" def change_email(req):sid = req.cookies.get("sid")user = SESSIONS.get(sid)if user is None:return 401if req.method != "POST":return 405if req.headers.get("Origin") != SITE:return 403sent = req.params.get("csrf", "")tok = user["csrf"]if not hmac.compare_digest(sent, tok):return 403user["email"] = req.params["email"]return 200

直した方は 2 つを確かめます。1 つ目は Origin ヘッダーで、ブラウザが要求の出どころとして付ける値です。OWASP によると、このヘッダーはページの JavaScript からは書き換えられません。2 つ目はトークンで、ログインのときに推測できない乱数で作り、自分のフォームに埋めておく値です。別のサイトのページは、こちらのページの中身を読めないので、この値を知りません。

Origin が付いていない要求は、ここでは 403 で断っています。OWASP は、HTTPS であっても Origin が必ず付くとは限らないと書いています。付かない要求を受ける必要がある画面では、Referer ヘッダー (どのページから来たか) の確かめに切り替えるか、トークンだけで判断します。トークンの比べに compare_digest を使うのは、比べにかかる時間から値を推し量られないためです。

脆いコード印の無いクッキー
def session_cookie(sid):return "sid=" + sid + "; Path=/"
直したコード3 つの印を付ける
def session_cookie(sid):return ("sid=" + sid + "; Path=/""; Secure; HttpOnly""; SameSite=Lax")

SameSite は、クッキーを別のサイトから始まった要求に付けるかどうかを決める印です。値は Strict、Lax、None の 3 つです。Strict は別のサイトからの要求には付けません。Lax は、リンクをたどるような、ページ全体の移動で安全な方式 (GET など) のときだけ付けます。None はいつでも付け、Secure の印が要ります。

OWASP は、Lax の限界をはっきり書いています。Lax は GET のページ移動ではクッキーを付けるので、状態を変える操作が GET で届くなら止まりません。最初の比べで POST だけを受けるようにしたのは、このためでもあります。

ログでの見え方と検知

CSRF の要求は、本人の要求と同じ形で届くので、ふつうの記録では見分けがつきません。直したコードで断った理由を記録に残すと、見えるようになります。

記録直したコードが断ったときの記録
change_email 403 reason=originorigin=http://127.0.0.1:9000user=1042

この行は、自分のサイトではない出どころから、ログイン済みの利用者の変更が届いたことを示します。守る側は、次の 3 つを記録と見張りに入れます。

見る物なぜ効くか
Origin とトークンで断った数ふだんの利用では、ほとんど出ない
断った要求の Origin の値どのサイトから送らされたかの手がかり
GET で状態が変わる口コードの見回りで、穴の候補になる

あわせて、メールアドレスやパスワードの変更のような大事な操作は、変わったことを本人に知らせます。送らされた変更でも、本人が気づけば早く戻せます。

直し方と、直ったことの確かめ方

自分の PC の中だけで行います。外のサービスや、他人の機械には向けません。

  1. 作業用のフォルダに、セッションを 1 つ持つ表と、上の関数を置く。ログインした利用者のメールアドレスを 1 つ入れておく
  2. 脆い関数に、同じクッキーを付けた GET の要求を渡し、メールアドレスが変わることを確かめる
  3. 別の出どころ (Origin が 127.0.0.1:9000) から、トークンの無い POST を渡し、これも変わることを確かめる
  4. 関数を直したコードに置き換え、2 と 3 が 405 と 403 で止まり、自分のページからの POST だけが通ることを確かめる
  5. 下の試験を足し、直す前のコードに戻すと試験が落ちることも確かめる
  6. 終わったら、作業用のフォルダごと消す
Python直ったことを固める試験
OTHER = "http://127.0.0.1:9000" def test_get_does_not_change(sid):req = Req("GET", {"email": "b@x.test"},{"sid": sid}, {"Origin": SITE})assert change_email(req) == 405 def test_other_origin_refused(sid):req = Req("POST", {"email": "b@x.test"},{"sid": sid}, {"Origin": OTHER})assert change_email(req) == 403

試験は「同じ要求で、直す前は変更が通り、直したあとは断られる」ことを固定します。あとで誰かが GET でも受ける書き方に戻しても、ここで止まります。

フレームワークに CSRF の守りが組み込まれているなら、まずそれを使います。OWASP も、自分で作る前に、組み込みの仕組みがあるかを確かめるよう勧めています。

関わる事例

IPA の「安全なウェブサイトの作り方」は、CSRF の届出の例として、ウェブアプリだけでなく、組み込み製品のウェブの管理画面にも同じ穴があった例を挙げています。管理画面は、ログインしたまま使われることが多い画面です。

根本的な解決として、IPA は 3 つの型を挙げています。処理を実行するページを POST で受け、前のページに埋めた秘密の値が正しいときだけ実行する型、実行の直前にパスワードを入れ直してもらう型、Referer が正しいリンク元のときだけ実行する型です。Referer の型は、攻撃者がそのサイトの上に罠を置けると効かない場合があり、Referer を送らない設定の利用者が使えなくなることもあると、IPA は注意しています。秘密の値は、暗号に使える乱数で、第三者に推測できないように作るよう書いています。このレッスンの直したコードは、前の型に Origin の確かめを重ねた物です。

確認クイズ

1 問目

セッションのクッキーに `SameSite=Lax` を付けました。設定の変更は GET の住所で受けています。どうなりますか。

  • ア Lax なので、別のサイトからの変更は止まる
  • イ 別のサイトからのリンクで移動すると、クッキーが付き、変更が通る
  • ウ GET にはクッキーが付かないので、ログインしていても変更できない
答えと理由を見る

答え イ

Lax は、ページ全体の移動で GET などの安全な方式なら、別のサイトから始まった要求にもクッキーを付けます。状態を変える操作が GET で届くと、Lax では止まりません。状態を変える操作は POST で受け、トークンで確かめます。

2 問目

トークンも Origin の確かめも入れたサイトに、XSS の穴が見つかりました。CSRF の守りはどうなりますか。

  • ア トークンがあるので、XSS があっても CSRF は防げる
  • イ ページの中で動くスクリプトはトークンを読めるので、CSRF の守りも越えられる
  • ウ XSS と CSRF は関係が無い
答えと理由を見る

答え イ

XSS のスクリプトは自分のサイトのページの中で動くので、ページに埋めたトークンを読み、自分のサイトの出どころから要求を送れます。CSRF の守りは「別のサイトからの要求」を止める物なので、ページの中からの要求は止めません。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書けたら、このレッスンは修了です。

  • XSS と CSRF の被害の出方の違い
模範解答を見る

XSS では、攻め手のスクリプトがそのサイトのページの中で動き、画面の中身やトークンを読み、本人として何でも送れる。CSRF では、攻め手は本人のブラウザに決まった要求を送らせるだけで、応答は読めない。だから XSS は出力のエスケープで、CSRF は要求の出どころの確かめで止め、XSS があれば CSRF の守りも越えられる。

一次資料

どれも 2026年10月24日に開いて、題と中身を確かめました。

次へ

次のレッスンは「認可の設計」です。本人であることを確かめたあと、その人がその操作をしてよいかを、すべての操作で確かめる作りを扱います。

次のレッスン 2-4 認可の設計

このレッスンにつながる物