# 機能設計書 17-CSRF保護

## 概要

本ドキュメントは、Ruby on RailsフレームワークにおけるCSRF（Cross-Site Request Forgery）保護機能の設計を記載する。CSRF保護は、悪意のあるサイトからの不正なリクエストを防ぎ、アプリケーションのセキュリティを確保する。

### 本機能の処理概要

CSRF保護は、リクエストの正当性を検証し、不正なクロスサイトリクエストを拒否する。現代のブラウザではSec-Fetch-Siteヘッダーを使用し、レガシーブラウザ向けにはトークンベースの検証をフォールバックとして提供する。

**業務上の目的・背景**：
CSRF攻撃は、ユーザーが認証済みの状態で悪意のあるサイトを訪問した際に、そのサイトからユーザーの意図しないリクエストが送信される攻撃である。例えば、銀行サイトにログイン中のユーザーが悪意のあるサイトを訪問すると、そのサイトから銀行への送金リクエストが自動的に送信される可能性がある。Rails標準のCSRF保護により、このような攻撃を防止する。

**機能の利用シーン**：
- フォーム送信時のトークン検証
- Ajax/XHRリクエストの検証
- Same-Origin/Same-Site検証
- 信頼されたオリジンからのリクエスト許可

**主要な処理内容**：
1. `protect_from_forgery`によるCSRF保護の有効化
2. Sec-Fetch-Siteヘッダーによるリクエスト検証
3. 認証トークンの生成とマスキング
4. トークンのセッション/クッキー保存
5. リクエスト時のトークン検証
6. 検証失敗時の処理（例外/セッションリセット/null_session）

**関連システム・外部連携**：
- Action Controller（before_action統合）
- セッション管理
- フォームヘルパー（`form_authenticity_token`）
- メタタグ（`csrf_meta_tags`）

**権限による制御**：
- CSRF保護自体が不正リクエストのフィルタリング機能
- API専用アプリケーションでは無効化可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 全画面共通 | 横断 | フォーム送信時のトークン検証 |

## 機能種別

セキュリティ / 認証 / リクエスト検証

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| authenticity_token | String | No | CSRFトークン（フォーム） | Base64形式 |
| X-CSRF-Token | String | No | CSRFトークン（ヘッダー） | Base64形式 |
| Sec-Fetch-Site | String | No | リクエスト元サイト関係 | same-origin/same-site/cross-site/none |

### 入力データソース

- HTTPリクエストヘッダー
- フォームパラメータ
- セッション（トークン保存）
- クッキー（代替保存）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| authenticity_token | String | 生成されたCSRFトークン |
| Vary | String | Sec-Fetch-Siteを含むVaryヘッダー |

### 出力先

- HTMLメタタグ
- フォームhidden field
- HTTPレスポンスヘッダー（Vary）

## 処理フロー

### 処理シーケンス

```
1. CSRF保護の設定
   └─ protect_from_forgery with: :exception
      └─ before_action :verify_request_for_forgery_protection

2. リクエスト検証
   └─ verified_request?
      ├─ GET/HEADリクエストはスキップ
      ├─ Sec-Fetch-Siteヘッダー検証
      │   └─ same-origin/same-site → 許可
      │   └─ cross-site → 拒否（trusted_origins除く）
      │   └─ none/missing → トークン検証にフォールバック
      └─ トークン検証（フォールバック時）
          └─ any_authenticity_token_valid?

3. 検証失敗時
   └─ handle_unverified_request
      ├─ :exception → InvalidCrossOriginRequest
      ├─ :reset_session → セッションリセット
      └─ :null_session → 空セッション提供
```

### フローチャート

```mermaid
flowchart TD
    A[HTTPリクエスト] --> B{GET/HEAD?}
    B -->|Yes| C[許可]
    B -->|No| D{protect_from_forgery?}
    D -->|No| C
    D -->|Yes| E{Sec-Fetch-Site?}
    E -->|same-origin/same-site| C
    E -->|cross-site| F{trusted_origins?}
    F -->|Yes| C
    F -->|No| G[拒否]
    E -->|none/missing| H{トークン検証}
    H -->|Valid| C
    H -->|Invalid| G
    G --> I[handle_unverified_request]
    I --> J{strategy}
    J -->|exception| K[InvalidCrossOriginRequest]
    J -->|reset_session| L[セッションリセット]
    J -->|null_session| M[空セッション]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | GETスキップ | GET/HEADリクエストは検証しない | 常時 |
| BR-002 | Sec-Fetch-Site優先 | モダンブラウザではヘッダーで検証 | header_only/header_or_legacy_token |
| BR-003 | トークンマスキング | BREACH攻撃対策でトークンをマスク | 常時 |
| BR-004 | Per-formトークン | フォームごとに異なるトークン生成可能 | per_form_csrf_tokens有効時 |
| BR-005 | Varyヘッダー | Sec-Fetch-SiteをVaryに追加 | 常時 |

### 計算ロジック

**トークン検証**：
- トークン長は32バイト（AUTHENTICITY_TOKEN_LENGTH）
- マスク済みトークンは64バイト（OTP + 暗号化トークン）
- HMAC-SHA256でグローバル/per-formトークンを生成
- `fixed_length_secure_compare`でタイミング攻撃を防止

## データベース操作仕様

CSRF保護はデータベースを操作しない。

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | なし |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| InvalidCrossOriginRequest | 例外 | CSRF検証失敗（:exception） | トークンを確認 |
| - | - | CSRF検証失敗（:reset_session） | セッションリセット |
| - | - | CSRF検証失敗（:null_session） | 空セッション提供 |

### リトライ仕様

特になし

## トランザクション仕様

特になし

## パフォーマンス要件

- トークン検証はリクエストごとに実行
- HMAC計算のオーバーヘッドは最小限
- Sec-Fetch-Siteヘッダー検証は高速

## セキュリティ考慮事項

- トークンはセッションに保存（セッションIDとは別）
- マスキングによるBREACH攻撃対策
- `secure_compare`によるタイミング攻撃対策
- per-formトークンによる更なる保護
- Originヘッダー検証オプション

## 備考

- Rails 8.0からSec-Fetch-Siteヘッダーベースの検証がデフォルト
- `ActionController::InvalidAuthenticityToken`は非推奨（`InvalidCrossOriginRequest`を使用）
- API専用アプリケーションではCSRF保護は不要
- `skip_forgery_protection`で特定アクションを除外可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: モジュール構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | RequestForgeryProtectionモジュール全体 |

**読解のコツ**: `included`ブロックで設定項目を確認し、ClassMethodsで`protect_from_forgery`の実装を理解する。

**主要処理フロー**（request_forgery_protection.rb）:
- **72行目**: CSRF_TOKENキー定義
- **79-140行目**: `included`ブロック、各種設定項目の定義
- **84行目**: `request_forgery_protection_token`デフォルト`:authenticity_token`
- **115行目**: `csrf_token_storage_strategy`デフォルト`SessionStore`
- **122行目**: `forgery_protection_verification_strategy`デフォルト`:header_or_legacy_token`
- **277-310行目**: `protect_from_forgery`メソッド
- **318-322行目**: `skip_forgery_protection`メソッド

#### Step 2: 検証ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | verified_request?周辺 |

**主要処理フロー**:
- **516-532行目**: `verify_request_for_forgery_protection`
- **620-623行目**: `verified_request?`
- **625-631行目**: `verified_request_for_forgery_protection?`
- **637-648行目**: `verified_via_header_only?`（Sec-Fetch-Site検証）
- **650-659行目**: `verified_with_legacy_token?`（トークン検証）
- **698-702行目**: `any_authenticity_token_valid?`

#### Step 3: トークン生成・検証を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | トークン関連メソッド |

**主要処理フロー**:
- **710-712行目**: `form_authenticity_token`
- **716-727行目**: `masked_authenticity_token`
- **731-759行目**: `valid_authenticity_token?`
- **761-766行目**: `unmask_token`
- **768-773行目**: `mask_token`
- **816-822行目**: `csrf_token_hmac`（HMAC-SHA256）

#### Step 4: ストレージ戦略を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | SessionStore/CookieStore |

**主要処理フロー**:
- **432-444行目**: `SessionStore`クラス
- **446-477行目**: `CookieStore`クラス

#### Step 5: 保護戦略を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | ProtectionMethods |

**主要処理フロー**:
- **366-430行目**: `ProtectionMethods`モジュール
- **367-407行目**: `NullSession`クラス
- **409-417行目**: `ResetSession`クラス
- **419-429行目**: `Exception`クラス

### プログラム呼び出し階層図

```
ApplicationController
    │
    └─ protect_from_forgery with: :exception
           │
           ├─ forgery_protection_strategy = Exception
           │
           └─ before_action :verify_request_for_forgery_protection
                  │
                  └─ verified_request?
                         │
                         ├─ request.get? || request.head?
                         │      └─ true → 許可
                         │
                         ├─ protect_against_forgery?
                         │      └─ false → 許可
                         │
                         └─ verified_request_for_forgery_protection?
                                │
                                ├─ :header_only → verified_via_header_only?
                                │      │
                                │      └─ sec_fetch_site_value
                                │             ├─ same-origin/same-site → true
                                │             ├─ cross-site → origin_trusted?
                                │             └─ nil → !request.ssl?
                                │
                                └─ :header_or_legacy_token → verified_with_legacy_token?
                                       │
                                       ├─ same-origin/same-site → true
                                       ├─ cross-site → origin_trusted?
                                       └─ none/missing → any_authenticity_token_valid?
                                              │
                                              └─ valid_authenticity_token?(session, token)
```

### データフロー図

```
[入力]                       [処理]                          [出力]

protect_from_forgery ───▶ 設定保存
                                │
                                ▼
HTTPリクエスト ───────────▶ verify_request_for_forgery_protection
     │                            │
     ├─▶ Sec-Fetch-Site ─────────▶ ヘッダー検証
     │                            │
     ├─▶ authenticity_token ─────▶ トークン検証
     │                            │
     └─▶ X-CSRF-Token ───────────▶ トークン検証
                                  │
                          ┌───────┴───────┐
                          │               │
                          ▼               ▼
                       許可           handle_unverified_request
                                          │
                                          ▼
                                  strategy.handle_unverified_request

form_authenticity_token
     │
     └─▶ masked_authenticity_token ───▶ HTMLフォーム / メタタグ
                │
                └─▶ real_csrf_token → HMAC → mask_token → Base64エンコード
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| request_forgery_protection.rb | `actionpack/lib/action_controller/metal/request_forgery_protection.rb` | ソース | CSRF保護モジュール |
| form_tag_helper.rb | `actionview/lib/action_view/helpers/form_tag_helper.rb` | ソース | authenticity_tokenヘルパー |
| csrf_helper.rb | `actionview/lib/action_view/helpers/csrf_helper.rb` | ソース | csrf_meta_tagsヘルパー |
| security_utils.rb | `activesupport/lib/active_support/security_utils.rb` | ソース | secure_compare |
