Webセキュリティ

CSRF(クロスサイトリクエストフォージェリ)対策|Webアプリの不正操作を防ぐ

セキュリティ CSRF Web

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の違い

項目XSSCSRF
攻撃の種類スクリプトの注入・実行不正リクエストの送信
攻撃対象ユーザーのブラウザサーバー側の処理
必要条件入力値の不適切な出力ユーザーのログイン状態
主な被害Cookie窃取、ページ改ざん不正な操作の実行

Step 2CSRF攻撃の具体例

例1:銀行の送金を悪用

被害者がオンラインバンキングにログインした状態で、攻撃者の罠サイトにアクセスした場合を考えます。

攻撃者の罠サイト(HTML)
<!-- 隠しフォームで自動送信 -->
<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タグだけで攻撃できます。

攻撃者の罠サイト(HTML)
<!-- 画像の読み込みに見せかけた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:パスワード変更の悪用

攻撃者の罠サイト(HTML)
<!-- 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(=トークン値)を読み取れないためです。

実装例

HTML(フォームにトークンを埋め込む)
<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>
Python(サーバー側の検証)
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ヘッダーにトークンを含めます。

JavaScript(Ajaxでのトークン送信)
// 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属性が必須)効果なし
サーバー側(Set-Cookieヘッダー)
# 推奨: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送信されない
AjaxPOST送信されない
imgタグGET送信されない
iframeの読み込みGET送信されない

SameSite CookieだけではCSRF対策として不十分です。古いブラウザではSameSite属性がサポートされていない場合があります。CSRFトークンと組み合わせて多層防御を行いましょう。

Step 5対策3:Refererヘッダーチェック

Refererヘッダーは、リクエスト元のURLを示すHTTPヘッダーです。サーバー側でリクエストの送信元が自サイトかどうかを検証できます。

Python(Refererチェックの実装例)
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よりも信頼性が高い場合があります。

Python(Originヘッダーも含めた検証)
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対策を提供します。

settings.py(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",
]
template.html(テンプレートでの使い方)
<!-- フォームには必ず {% csrf_token %} を含める -->
<form method="POST" action="/transfer/">
  {% csrf_token %}
  <input type="text" name="to">
  <button type="submit">送金</button>
</form>
JavaScript(DjangoでのAjaxリクエスト)
// 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対策が有効です。

application_controller.rb
class ApplicationController < ActionController::Base
  # デフォルトで有効(CSRF トークンを検証)
  protect_from_forgery with: :exception
end
ERB テンプレート
<!-- レイアウトの <head> に meta タグを追加 -->
<%= csrf_meta_tags %>

<!-- フォームヘルパーを使えば自動でトークンが含まれる -->
<%= form_with url: transfer_path do |f| %>
  <%= f.text_field :to %>
  <%= f.submit "送金" %>
<% end %>
JavaScript(RailsでのAjaxリクエスト)
// 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ミドルウェアがデフォルトで有効です。

Blade テンプレート
<!-- フォームには @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() }}">
JavaScript(LaravelでのAjaxリクエスト)
// 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),
});
app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
    // CSRF検証を除外するURL(API等)
    protected  = [
        "api/webhook/*",   // Webhook は外部からのリクエスト
        "api/stripe/*",    // 決済コールバック
    ];
}

フレームワーク比較まとめ

機能DjangoRailsLaravel
デフォルトで有効はいはいはい
テンプレートタグ{% csrf_token %}csrf_meta_tags@csrf
AjaxヘッダーX-CSRFTokenX-CSRF-TokenX-CSRF-TOKEN
除外方法@csrf_exemptskip_before_action
トークン保存先Cookie + hidden fieldSession + meta tagSession + Cookie

CheckCSRF対策チェックリスト

多層防御が重要:CSRFトークン、SameSite Cookie、Origin/Refererチェックの3つを組み合わせることで、堅牢なCSRF対策を実現できます。フレームワークのデフォルト設定を活かしつつ、追加の設定で防御力を高めましょう。