概要・この記事で伝えること
Webアプリケーションのセキュリティ用語として「CSRF(Cross-Site Request Forgery、クロスサイトリクエストフォージェリ)」という言葉を耳にしたことがある方は多いと思います。しかし、「XSSと何が違うの?」「Cookie認証だと何がまずいの?」ときちんと説明できるかというと、意外とあやふやなまま使っている人も多いのではないでしょうか。
この記事では、
- CSRF攻撃がどういう仕組みで成立するのか
- なぜブラウザのCookie自動送信が原因になるのか
- どうやって防ぐのか(トークン方式・SameSite属性)
を、実際のリクエストの流れを追いながら解説します。読み終える頃には「Cookieだけで認証すると何が危ういのか」を自分の言葉で説明できるようになるはずです。
基礎知識の解説
CSRFとは何か
CSRFは、ログイン済みのユーザーのブラウザを「踏み台」にして、ユーザーの意図しないリクエストを正規のサイトに送りつける攻撃です。
ポイントは、攻撃者はユーザーのパスワードやセッションIDを盗む必要がないということです。ユーザーがすでに正規サイトにログインしている状態(=有効なセッションCookieを持っている状態)につけこみます。
なぜCookieが問題になるのか
ブラウザは、あるドメインへのリクエストを送るとき、そのドメインに紐づくCookieを自動的に付与します。これはCookieの仕様上の基本動作であり、リクエストがどのページ(どのサイト)から発生したものかは基本的に区別しません。
つまり以下のような状況が成立します。
- ユーザーが
bank.example.comにログインし、セッションCookieがブラウザに保存される - ユーザーが別タブで悪意あるサイト
evil.example.comを開く -
evil.example.com内にbank.example.comへの送金リクエストを自動送信する仕掛け(フォームの自動送信やimgタグなど)が仕込まれている - ブラウザは
bank.example.com宛のリクエストだと判断し、律儀にセッションCookieを添付して送信してしまう -
bank.example.comのサーバーは「有効なセッションCookieが付いている」ことしか見ていないため、正規のリクエストとして処理してしまう
サーバー側から見ると、リクエストの送信元が evil.example.com 経由であることに気づかない限り、これは「本人からの正規リクエスト」と区別がつきません。ここがCSRFの本質的な怖さです。
攻撃の流れを図解する
XSSとの違い
CSRFとよく混同されるのがXSS(Cross-Site Scripting)です。両者は狙う場所が異なります。
| 攻撃対象 | やること | |
|---|---|---|
| XSS | 脆弱なサイト自体 | 悪意あるスクリプトをサイトに注入し、任意のコードを実行させる |
| CSRF | サイトを利用中のユーザー | ユーザーのブラウザに「本人になりすましたリクエスト」を送らせる |
XSSはサイトの脆弱性そのものを突きますが、CSRFはサイトのリクエスト処理の設計(=Cookieだけで本人確認をしていること)を突きます。そのためCSRFはサイト側にXSSのような明確な脆弱性コードがなくても成立しうる点が厄介です。
具体例・実際の動作
脆弱なフォーム処理(悪い例)
サーバー側が「セッションCookieが有効かどうか」だけをチェックしているケースです。
# 脆弱な例: Cookieの有無だけで本人確認したことにしている
from fastapi import FastAPI, Cookie, HTTPException
app = FastAPI()
@app.post("/transfer")
def transfer_money(amount: int, to_account: str, session_id: str = Cookie(None)):
if not is_valid_session(session_id):
raise HTTPException(status_code=401)
# ここでCSRFトークンの検証を一切していない
execute_transfer(session_id, amount, to_account)
return {"status": "ok"}
このエンドポイントに対して、悪意あるサイト側は次のようなHTMLを仕込むだけで攻撃が成立してしまいます。
<form action="https://bank.example.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="amount" value="100000">
<input type="hidden" name="to_account" value="attacker-account">
</form>
<script>
document.getElementById("csrf-form").submit();
</script>
対策1: CSRFトークン方式
正規のページを表示するときに、サーバーがランダムなCSRFトークンを発行し、フォームに埋め込みます。リクエスト送信時にはこのトークンも一緒に送らせ、サーバー側でセッションに紐づくトークンと一致するか検証します。
import secrets
@app.get("/transfer-form")
def show_transfer_form(session_id: str = Cookie(None)):
csrf_token = secrets.token_urlsafe(32)
save_csrf_token_for_session(session_id, csrf_token)
return render_form(csrf_token=csrf_token)
@app.post("/transfer")
def transfer_money(
amount: int,
to_account: str,
csrf_token: str,
session_id: str = Cookie(None),
):
if not is_valid_session(session_id):
raise HTTPException(status_code=401)
if not verify_csrf_token(session_id, csrf_token):
raise HTTPException(status_code=403, detail="invalid csrf token")
execute_transfer(session_id, amount, to_account)
return {"status": "ok"}
悪意あるサイト側は、このcsrf_tokenの正しい値を知る手段がありません(同一オリジンポリシーにより、他ドメインのページのレスポンス内容を読み取ることはできないため)。したがって偽装リクエストにはトークンを含めることができず、サーバー側の検証で弾かれます。
対策2: Cookieの SameSite 属性
より根本的な対策として、Cookie発行時に SameSite 属性を指定する方法があります。
Set-Cookie: session_id=abc123; SameSite=Strict; Secure; HttpOnly
-
SameSite=Strict: 他サイトからのリクエストには一切Cookieを付与しない -
SameSite=Lax: 通常のリンク遷移(GET)では付与するが、他サイトからのフォーム自動送信(POST等)では付与しない -
SameSite=None: 従来通り常に付与する(Secure属性とセットでの指定が必須)
昨今の主要ブラウザはデフォルトを Lax 相当として扱うようになっており、これだけでもCSRFの成立条件をかなり狭められます。ただし完全に防げるわけではないため、重要な操作(送金・退会・権限変更など)にはCSRFトークンとの併用が推奨されます。
なお、Chrome/Chromium系ブラウザには「Lax + POST」と呼ばれる緩和措置があり、Cookie発行から2分間はクロスサイトのトップレベルPOSTナビゲーションでもLaxの制限が適用されずSameSite=None相当としてCookieが送信されます(OAuthコールバックなど正規のクロスサイト遷移との互換性維持が目的の実装です)。つまり「SameSite未指定=デフォルトLaxだから安心」とは言い切れず、発行直後の一定時間は依然として攻撃対象になり得る点に注意が必要です。
筆者の考え・所感
個人的にCSRFがおもしろいと感じるのは、「攻撃者は何も盗んでいない」という点です。パスワードもセッションIDも一切手元になく、ただユーザーのブラウザに"いつも通りの動作"をさせているだけで攻撃が成立してしまう。ブラウザの正しい仕様(Cookieの自動送信)が、そのまま攻撃の土台になってしまうというのは、セキュリティを考えるうえで示唆的だと思います。
実務では、FastAPIやDjangoなどのフレームワークがCSRF対策のミドルウェアを標準で用意していることが多く、「フレームワークの提供する仕組みに任せておけば安全」と思い込みがちです。ですが、APIをフロントエンドと分離して自前で実装するケースや、独自のCookie発行ロジックを書くケースでは、この保護が抜け落ちていることに気づきにくいです。自分がAPIを設計するときは、「このエンドポイントは"Cookieが付いている"以外に、リクエストの正当性をどう担保しているか」を必ず自問するようにしています。
また、SameSite=Lax がデフォルト化されて以降、CSRFの脅威はだいぶ下がったと言われますが、これは「対策済みだから考えなくていい」ではなく「基礎防御ラインが上がっただけ」と捉えるのが安全だと感じています。重要な操作にはやはりCSRFトークンを重ねておくのが、個人的には安心できる設計です。
まとめ
- CSRFは、ログイン済みユーザーのブラウザを踏み台にして、意図しないリクエストを正規サイトに送らせる攻撃
- ブラウザがCookieをドメイン単位で自動付与する仕様が根本原因であり、サーバー側が「Cookieの有無」だけで本人確認をしていると成立してしまう
- CSRFトークンの検証と、Cookieの
SameSite属性の指定を組み合わせることで対策する