こんにちは!プロトコルの奥深い世界へようこそ。
WebアプリケーションやREST APIを作っていると、必ず耳にするセキュリティ用語がいくつかありますよね。その中でも、初学者の方が「名前は知っているけれど、具体的にどう危なくて、どう守ればいいのかピンとこない……」と悩みやすい代表格がCSRF(Cross-Site Request Forgery:クロスサイト・リクエスト・フォージェリ)です。
今回は、ネットワークやWeb開発の扉を叩いたばかりのエンジニアの皆さんに向けて、CSRFの根本的な仕組みと、それを防ぐ2大防御策である「SameSite属性」と「Anti-CSRFトークン」について、現実の郵便配達や銀行の手続きに例えながら、一歩ずつ優しく紐解いていきます。
難解な数式や小難しいビット演算は出てきませんので、リラックスして読み進めてくださいね!
—
1. そもそもCSRF(身代わりリクエスト詐欺)とは?
まずは敵を知るところから始めましょう。
CSRFを日本語に直訳すると「サイトをまたいだリクエストの偽造」となります。一言で言うと「悪意あるサイトが、あなた(被害者)のブラウザを踏み台にして、勝手に別のWebサービスへリクエストを送りつけてしまう攻撃」です。
現実世界で例えると:勝手に署名捺印された「送金依頼書」
身近な銀行の手続きでイメージしてみましょう。
1. あなたは「A銀行」にログインし、自分専用の「実印(ログイン中のセッションCookie)」をブラウザに持っています。
2. その状態のまま、たまたま怪しい罠サイト(Bサイト)のリンクをクリックして開いてしまいました。
3. 罠サイトのページには、実は「A銀行の口座から、攻撃者の口座へ10万円振り込め!」という依頼書(POSTリクエスト)がこっそり仕込まれています。
4. ブラウザはとても親切で実直な配達員なので、「A銀行宛ての手紙だな!よし、いつも通り本人の実印(Cookie)を添えて送っておこう!」と、自動的にCookieを添えてA銀行に手紙を届けてしまいます。
5. A銀行のサーバーは、「ちゃんと本人の実印が押してある正規の依頼だな」と判断し、10万円を振り込んでしまいます。
これがCSRFの正体です。攻撃者はあなたのIDやパスワードを盗み出したわけではありません。ブラウザが持っている「正規の認証情報(Cookie)」を無断で利用(悪用)させただけなのです。
ブラウザの「自動でCookieを添えて送る親切心」が、セキュリティの落とし穴になってしまうわけですね。
—
2. 防御策①:ブラウザの玄関で防ぐ「CookieのSameSite属性」
この問題に対して、「そもそもよそのWebサイトから送られるリクエストには、自動で実印(Cookie)を添えないでほしい!」という発想から生まれたのが、CookieのSameSite属性です。
サーバーからブラウザにCookieを渡す際(Set-Cookieヘッダー)、このSameSite属性をつけておくことで、ブラウザに「この実印を他所からの手紙に添えていいかどうか」のルールを指示できます。
主に使われる選択肢は以下の3種類です。
| 設定値 | 挙動のイメージ | どんなときに使う? |
| :— | :— | :— |
| SameSite=Strict | 【完全門前払い】
別サイトからのリンクを踏んでやってきた場合、Cookieは一切添えない。 | 銀行の送金画面や管理画面など、極めて高い安全性が求められるAPI。 |
| SameSite=Lax | 【日常使いの標準】
別サイトからの「単なるページ遷移(リンクをクリックしたGET)」ならCookieを添えるが、フォーム送信(POST)などの書き換えリクエストでは添えない。 | 一般的なWebサイトやAPI。現在の主要ブラウザのデフォルト挙動。 |
| SameSite=None | 【制限なし】
どこのサイトから送られるリクエストにもCookieを添える。(※必ずSecure属性=HTTPSが必須) | 外部サイトに埋め込んで使うウィジェットやクロスオリジンの特殊な連携。 |
郵便配達で例えると?
Strict(厳格):
「うちの敷地の中から投函された手紙以外には、絶対に実印スタンプを押しちゃダメ!」というルールです。外部サイトのリンクから飛んできた瞬間は「未ログイン状態」に見えるため安全性は最高ですが、ユーザー体験として少し不便になることもあります。
Lax(緩やか):
「外から見に来ただけ(閲覧用のGET)ならスタンプを押してあげるけれど、外から『データを書き換える荷物(POSTなど)』を送ろうとするならスタンプは絶対押さないよ!」というバランスの良いルールです。
サーバーでの設定例
Webサーバー(例: Nginx)や各種言語のレスポンスヘッダーで、以下のように設定します。
# HTTPレスポンスヘッダーの例
Set-Cookie: session_id=xyz789abc123; Path=/; Secure; HttpOnly; SameSite=Lax
※ Secure(HTTPS通信時のみ送信)と HttpOnly(JavaScriptからの不正読み取り防止)も必ず一緒にセットで指定するのが、プロの現場のお約束です。
—
3. 防御策②:合言葉で照合する「Anti-CSRFトークン」
SameSite=Laxが現代のブラウザの標準になったことで、多くのCSRF攻撃は未然に防げるようになりました。
しかし、古いブラウザを使うユーザーの存在や、サブドメイン間の関係性、モバイルアプリ向けWebViewの挙動などを考慮すると、「Cookieの挙動だけに命を預けるのは危険(単一障害点になる)」とインフラアーキテクトは考えます。
そこで登場するもうひとつの強力な盾が、「Anti-CSRFトークン(合言葉)」です。
合言葉の仕組み
1. ユーザーが正規の画面(例:送金フォーム)を開いたとき、サーバーは「今回限りのランダムな文字列(トークン)」を発行し、画面の中に埋め込んでおきます。
2. ユーザーが「送金ボタン」を押したとき、ブラウザは「実印(Cookie)」と一緒に「合言葉(トークン)」もサーバーへ送ります。
3. サーバー側で、「手紙に貼られた実印」だけでなく「届いた合言葉が、先ほど発行したものと一致するか?」をダブルチェックします。
攻撃者が用意した罠サイトは、A銀行の画面を開かせずに裏でリクエストだけを偽造して飛ばすため、「今有効な合言葉」を知ることができません。
結果として、サーバー側で「Cookieはあるけれど、合言葉が抜けている(または違う)ぞ!」と検知し、攻撃をブロックできるわけです。
—
4. 実践:REST APIにおける美しいCSRF対策の実装
近年のSPA(Single Page Application)やREST APIでは、従来のHTMLフォーム送信ではなく、JavaScript(Fetch APIやAxiosなど)を使ったJSON形式での通信が主流です。
この構成では、「Cookieにトークンを入れて配り、リクエスト時にはそれをカスタムヘッダーに載せて送り返してもらう」というアプローチ(Double Submit Cookieパターンなど)がよく使われます。
ここでは、軽量で読みやすいPythonの「FastAPI」を用いた実装例を見てみましょう。
バックエンド(APIサーバー)の実装例
import secrets
from fastapi import FastAPI, Depends, HTTPException, Header, Response, Cookie
from typing import Optional
app = FastAPI(title="CSRF対策デモAPI")
# 1. ユーザーがページにアクセスした際に、使い捨てのCSRFトークンを発行するエンドポイント
@app.get("/api/csrf-token")
def get_csrf_token(response: Response):
# 暗号学的に安全なランダム文字列(合言葉)を生成
token = secrets.token_urlsafe(32)
# クライアントのJavaScriptが読み取れるようにHttpOnlyはFalseにし、SameSite=Laxを設定
response.set_cookie(
key="csrf_token",
value=token,
httponly=False, # JS側で読み取ってヘッダーにセットさせるため
samesite="lax",
secure=True # 本番環境ではHTTPS必須
)
return {"message": "トークンを発行しました"}
# 2. トークンを検証するための共通関数(依存性注入)
def verify_csrf(
x_csrf_token: Optional[str] = Header(None), # リクエストヘッダーから取得
csrf_token: Optional[str] = Cookie(None) # Cookieから取得
):
# ヘッダーまたはCookieのどちらかが欠けている、あるいは値が一致しない場合はブロック
if not x_csrf_token or not csrf_token or x_csrf_token != csrf_token:
raise HTTPException(
status_code=403,
detail="CSRFトークンが無効、または存在しません。正規の画面からやり直してください。"
)
return True
# 3. 重要な処理を行うエンドポイント(POSTやPUTなど)
@app.post("/api/transfer", dependencies=[Depends(verify_csrf)])
def transfer_money(data: dict):
# verify_csrf による合言葉チェックを通過した安全なリクエストだけがここを実行できる
return {
"status": "success",
"message": f"{data.get('amount')}円の送金が安全に完了しました!"
}
フロントエンド(JavaScript / ブラウザ側)の挙動例
フロントエンドからは、Cookieに入っている合言葉を取り出し、HTTPリクエストのヘッダーにX-CSRF-Tokenとして添えて送信します。
// Cookieから指定した名前の値を取り出す関数
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
async function sendMoney() {
// 1. Cookieから合言葉(CSRFトークン)を拾い出す
const csrfToken = getCookie('csrf_token');
// 2. リクエストのカスタムヘッダーに合言葉を添えてAPIを叩く
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // ← ここで合言葉を証明書として提示!
},
body: JSON.stringify({
recipient: "friend_account",
amount: 10000
})
});
const result = await response.json();
console.log(result);
}
攻撃者のサイトから勝手にfetchやフォームで通信を飛ばそうとしても、ブラウザの「同一生成元ポリシー(Same-Origin Policy)」によって、攻撃者は別サイトのCookieの中身(合言葉)を盗み見ることができません。
そのため、攻撃者が送るリクエストにはX-CSRF-Tokenヘッダーを載せることができず、サーバー側で「不審者」として確実にシャットアウトできるのです。
—
5. まとめ:多層防御で安全で美しいWeb APIを
最後に、今回学んだポイントを振り返ってみましょう。
1. CSRF攻撃は、ブラウザが「保存しているCookieを親切に自動で添えて送ってしまう仕組み」を悪用した身代わり詐欺。
2. SameSite属性(Lax / Strict)は、他所から持ち込まれたリクエストにCookieを自動で添えないようにする、ブラウザ標準の防御壁。
3. Anti-CSRFトークンは、正規のクライアントしか知り得ない「一時的な合言葉」をリクエストヘッダーに載せさせて確認する、アプリケーション側の強固な盾。
ネットワークやWebセキュリティの世界では、「これを1つやっておけば100%安心!」という魔法の杖は滅多にありません。
ブラウザの盾(SameSite属性)とサーバーの盾(Anti-CSRFトークン)を組み合わせる多層防御(Defense in Depth)の考え方こそが、堅牢で信頼されるシステムを作るための第一歩です。
プロトコルの基本と仕組みを正しく押さえて、安全で使いやすいAPI設計を楽しんでいきましょう!
コメント