CSRF(クロスサイトリクエストフォージェリ)対策
Webアプリの不正操作を防ぐ
CSRF(Cross-Site Request Forgery)は、ユーザーが意図しないリクエストを別サイトから送信させる攻撃手法で、OWASP Top 10にも含まれる代表的な脆弱性です。
この記事では、CSRF攻撃の仕組みから具体的な防御方法まで、実践的に解説します。
こんな人向けの記事です
- CSRF攻撃の仕組みを理解したい
- 自分のWebアプリにCSRF脆弱性がないか確認したい
- CSRFトークンやSameSite Cookieの設定方法を知りたい
- Django・Rails・Laravelでの対策を知りたい
Step 1CSRFとは何か(攻撃の仕組み)
CSRF(Cross-Site Request Forgery)は、ログイン済みユーザーのブラウザを悪用して、ユーザーの意図しない操作(送金、設定変更、データ削除など)を実行させる攻撃です。
正規サイトにログイン中
不正リクエストを仕込む
認証済みとして処理実行
なぜCSRFが成立するのか
ブラウザは、リクエスト送信時にそのドメインのCookieを自動的に付与します。攻撃者のサイトから正規サイトへのリクエストでも、ユーザーのセッションCookieが送信されるため、サーバーは「正規ユーザーからのリクエスト」と判断してしまいます。
XSSとCSRFの違い
| 項目 | XSS | CSRF |
|---|---|---|
| 攻撃の種類 | スクリプトの注入・実行 | 不正リクエストの送信 |
| 攻撃対象 | ユーザーのブラウザ | サーバー側の処理 |
| 必要条件 | 入力値の不適切な出力 | ユーザーのログイン状態 |
| 主な被害 | Cookie窃取、ページ改ざん | 不正な操作の実行 |
Step 2CSRF攻撃の具体例
例1:銀行の送金を悪用
被害者がオンラインバンキングにログインした状態で、攻撃者の罠サイトにアクセスした場合を考えます。
<!-- 隠しフォームで自動送信 -->
<form action="https://bank.example.com/transfer" method="POST" id="evil-form">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="1000000">
</form>
<script>document.getElementById("evil-form").submit();</script>
被害者は何も操作していないのに、ページを開いただけで送金リクエストが送信されます。サーバーはセッションCookieで認証を確認するため、正規のリクエストとして処理してしまいます。
例2:GETリクエストを悪用
状態変更をGETで受け付けている場合、imgタグだけで攻撃できます。
<!-- 画像の読み込みに見せかけたGETリクエスト -->
<img src="https://example.com/api/delete-account?confirm=yes" style="display:none">
<!-- メールやSNSのリンクでも攻撃可能 -->
<a href="https://example.com/settings/change-email?email=attacker@evil.com">
こちらをクリック
</a>
重要:GETリクエストで状態変更(削除、更新など)を行うAPIは絶対に作らないでください。GETはデータ取得のみに使い、状態変更はPOST/PUT/DELETEで行いましょう。
例3:パスワード変更の悪用
<!-- iframeで隠してパスワードを変更 -->
<iframe style="display:none" name="csrf-frame"></iframe>
<form action="https://example.com/change-password" method="POST" target="csrf-frame">
<input type="hidden" name="new_password" value="hacked123">
<input type="hidden" name="confirm_password" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
Step 3対策1:CSRFトークン
CSRFトークンは、最も広く使われている対策方法です。サーバーがランダムなトークンを発行し、フォーム送信時にそのトークンを検証します。
仕組み
ランダムなトークン生成
hidden fieldにトークン埋め込み
トークンを検証
なぜトークンが有効なのか
攻撃者は別サイトからリクエストを送信しますが、正規サイトのHTMLに埋め込まれたトークン値を知ることができません。同一オリジンポリシーにより、攻撃者のJavaScriptは正規サイトのDOM(=トークン値)を読み取れないためです。
実装例
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="a8f9d3e1b7c2...">
<input type="text" name="to" placeholder="送金先">
<input type="number" name="amount" placeholder="金額">
<button type="submit">送金</button>
</form>
import secrets
# トークン生成(セッションに保存)
def generate_csrf_token(session):
token = secrets.token_hex(32)
session["csrf_token"] = token
return token
# トークン検証
def validate_csrf_token(session, form_token):
expected = session.get("csrf_token")
if not expected or not secrets.compare_digest(expected, form_token):
raise PermissionError("CSRF token validation failed")
# トークンを使い捨てにする(推奨)
del session["csrf_token"]
Ajax(非同期リクエスト)の場合
Ajaxリクエストでは、カスタムHTTPヘッダーにトークンを含めます。
// metaタグからトークンを取得
const token = document.querySelector("meta[name='csrf-token']").content;
fetch("/api/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token // カスタムヘッダーで送信
},
body: JSON.stringify({ to: "user123", amount: 5000 })
});
カスタムヘッダーが有効な理由:ブラウザの同一オリジンポリシーにより、別オリジンからのリクエストにカスタムヘッダーを付けることはできません(CORSのプリフライトで拒否される)。そのため、カスタムヘッダーの存在自体がCSRF対策になります。
Step 4対策2:SameSite Cookie
SameSite属性は、Cookieの送信条件をブラウザレベルで制御する仕組みです。別サイトからのリクエストにCookieを付与しないようにできます。
3つのモード
| 値 | 動作 | CSRF対策効果 |
|---|---|---|
Strict | 同一サイトからのリクエストにのみCookieを送信 | 最も強力 |
Lax | トップレベルナビゲーション(リンク遷移)のGETのみ許可 | 実用的なバランス |
None | すべてのリクエストにCookieを送信(Secure属性が必須) | 効果なし |
# 推奨:Lax(ほとんどのケースで適切)
Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly
# 厳格:Strict(外部リンクからのアクセスでもCookieが送られない)
Set-Cookie: session_id=abc123; SameSite=Strict; Secure; HttpOnly
# 非推奨:None(サードパーティCookieが必要な場合のみ)
Set-Cookie: session_id=abc123; SameSite=None; Secure; HttpOnly
SameSite=Lax の詳細な挙動
| リクエストの種類 | メソッド | Cookie送信 |
|---|---|---|
| 通常のリンク遷移 | GET | 送信される |
| フォーム送信 | POST | 送信されない |
| Ajax | POST | 送信されない |
| imgタグ | GET | 送信されない |
| iframeの読み込み | GET | 送信されない |
SameSite CookieだけではCSRF対策として不十分です。古いブラウザではSameSite属性がサポートされていない場合があります。CSRFトークンと組み合わせて多層防御を行いましょう。
Step 5対策3:Refererヘッダーチェック
Refererヘッダーは、リクエスト元のURLを示すHTTPヘッダーです。サーバー側でリクエストの送信元が自サイトかどうかを検証できます。
from urllib.parse import urlparse
ALLOWED_ORIGINS = ["https://example.com", "https://www.example.com"]
def check_referer(request):
referer = request.headers.get("Referer")
if not referer:
# Refererがない場合は拒否(厳格モード)
return False
parsed = urlparse(referer)
origin = f"{parsed.scheme}://{parsed.netloc}"
return origin in ALLOWED_ORIGINS
Refererチェックの注意点
| 注意点 | 詳細 |
|---|---|
| Refererが送信されないケース | ブラウザの設定やプライバシー拡張機能で抑制される場合がある |
| Referrer-Policyの影響 | no-referrer設定の場合、Refererが送信されない |
| HTTPからHTTPSへの遷移 | HTTPページからHTTPSページへのリクエストではRefererが送信されないことがある |
| 単独では不十分 | Refererの偽装は難しいが、補助的な対策として使用すべき |
Originヘッダーも活用しよう:Originヘッダーは、Refererよりもプライバシーに配慮した形で送信元オリジンを示します。POST/PUT/DELETEリクエストでは通常送信されるため、Refererよりも信頼性が高い場合があります。
def check_origin(request):
# Originヘッダーを優先的にチェック
origin = request.headers.get("Origin")
if origin:
return origin in ALLOWED_ORIGINS
# Originがなければ Referer をフォールバック
referer = request.headers.get("Referer")
if referer:
parsed = urlparse(referer)
req_origin = f"{parsed.scheme}://{parsed.netloc}"
return req_origin in ALLOWED_ORIGINS
# どちらもない場合は拒否
return False
Step 6フレームワーク別の対策(Django・Rails・Laravel)
Django
Djangoは、CSRFミドルウェアがデフォルトで有効になっており、最も手厚いCSRF対策を提供します。
# CSRF ミドルウェア(デフォルトで有効)
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", # これがCSRF対策
# ...
]
# 本番環境での推奨設定
CSRF_COOKIE_SECURE = True # HTTPS のみで Cookie を送信
CSRF_COOKIE_HTTPONLY = True # JavaScript からアクセス不可
CSRF_COOKIE_SAMESITE = "Lax" # SameSite 属性
CSRF_TRUSTED_ORIGINS = [ # 信頼するオリジン
"https://example.com",
"https://www.example.com",
]
<!-- フォームには必ず {% csrf_token %} を含める -->
<form method="POST" action="/transfer/">
{% csrf_token %}
<input type="text" name="to">
<button type="submit">送金</button>
</form>
// Cookieからトークンを取得する関数
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + "=")) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
fetch("/api/endpoint/", {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
@csrf_exempt は極力使わない:APIエンドポイントなどで @csrf_exempt を使う場合は、代わりにトークン認証(JWT等)や API キー認証を導入して、別の方法でリクエストの正当性を担保してください。
Ruby on Rails
Railsも、デフォルトでCSRF対策が有効です。
class ApplicationController < ActionController::Base
# デフォルトで有効(CSRF トークンを検証)
protect_from_forgery with: :exception
end
<!-- レイアウトの <head> に meta タグを追加 -->
<%= csrf_meta_tags %>
<!-- フォームヘルパーを使えば自動でトークンが含まれる -->
<%= form_with url: transfer_path do |f| %>
<%= f.text_field :to %>
<%= f.submit "送金" %>
<% end %>
// metaタグからトークンを取得
const token = document.querySelector("meta[name='csrf-token']").content;
fetch("/api/endpoint", {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
Laravel
Laravelも、CSRFミドルウェアがデフォルトで有効です。
<!-- フォームには @csrf ディレクティブを含める -->
<form method="POST" action="/transfer">
@csrf
<input type="text" name="to">
<button type="submit">送金</button>
</form>
<!-- meta タグに CSRF トークンを設定 -->
<meta name="csrf-token" content="{{ csrf_token() }}">
// Axiosを使う場合(Laravel Mixのデフォルト設定)
// resources/js/bootstrap.js で自動設定される
window.axios.defaults.headers.common["X-CSRF-TOKEN"] =
document.querySelector("meta[name='csrf-token']").content;
// fetchを使う場合
fetch("/api/endpoint", {
method: "POST",
headers: {
"X-CSRF-TOKEN": document.querySelector("meta[name='csrf-token']").content,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
class VerifyCsrfToken extends Middleware
{
// CSRF検証を除外するURL(API等)
protected = [
"api/webhook/*", // Webhook は外部からのリクエスト
"api/stripe/*", // 決済コールバック
];
}
フレームワーク比較まとめ
| 機能 | Django | Rails | Laravel |
|---|---|---|---|
| デフォルトで有効 | はい | はい | はい |
| テンプレートタグ | {% csrf_token %} | csrf_meta_tags | @csrf |
| Ajaxヘッダー | X-CSRFToken | X-CSRF-Token | X-CSRF-TOKEN |
| 除外方法 | @csrf_exempt | skip_before_action | |
| トークン保存先 | Cookie + hidden field | Session + meta tag | Session + Cookie |
CheckCSRF対策チェックリスト
多層防御が重要:CSRFトークン、SameSite Cookie、Origin/Refererチェックの3つを組み合わせることで、堅牢なCSRF対策を実現できます。フレームワークのデフォルト設定を活かしつつ、追加の設定で防御力を高めましょう。