# 機能設計書 90-ブルートフォース対策

## 概要

本ドキュメントは、「ブルートフォース対策」機能の設計を定義する。ブルートフォース対策機能は、ログイン試行回数制限、パスワードリセット制限、その他の認証関連エンドポイントへのレートリミットを実装し、不正アクセスやスパム攻撃からシステムを保護する。

### 本機能の処理概要

ブルートフォース対策は、express-bruteライブラリとbrute-knex（データベースストア）を使用して、IPアドレスまたはユーザー単位でリクエスト数を追跡し、閾値を超えた場合にアクセスを一時的にブロックする。

**業務上の目的・背景**：Ghost AdminおよびAPI認証エンドポイントを、パスワード総当たり攻撃やユーザー列挙攻撃から保護する。また、Content API、メンバー認証、Webmention受信などの公開エンドポイントに対するスパム攻撃も防止する。

**機能の利用シーン**：
- スタッフユーザーのログイン試行制限
- パスワードリセットリクエスト制限
- 2FA検証コードの送信・検証制限
- メンバー認証の試行制限
- Content APIキーの不正使用防止
- プライベートブログのパスワード入力制限
- Webmention受信のスパム対策
- プレビューメール送信のレートリミット

**主要な処理内容**：
1. リクエスト元（IPアドレスまたはキー）の識別
2. bruteテーブルでの試行回数の記録・追跡
3. 閾値超過時のリクエスト拒否（429 Too Many Requests）
4. 成功時のカウンタリセット（一部エンドポイント）
5. フィボナッチ数列に基づく待機時間の増加

**関連システム・外部連携**：
- express-brute - レートリミットミドルウェア
- brute-knex - データベースストアアダプタ
- 認証サービス - ログイン・パスワードリセット

**権限による制御**：認証前のエンドポイントに適用。認証成功後は対象外。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | サインイン画面 | 主画面 | ログイン試行制限 |
| 4 | パスワードリセット画面 | 主画面 | パスワードリセット制限 |
| 2 | サインイン確認画面 | 副画面 | 2FA検証試行制限 |

## 機能種別

セキュリティ / ミドルウェア

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| IP Address | string | Yes | req.ipから取得 | - |
| key | string | No | 制限種別+識別子（email等） | - |
| config.spam | object | No | spam設定オブジェクト | - |

### 入力データソース

- HTTPリクエスト（req.ip）
- Ghost設定ファイル（config.spam）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| TooManyRequestsError | Error | 429エラーレスポンス |
| nextValidRequestDate | Date | 次回リクエスト可能日時 |
| req.brute.reset | function | リセット関数（成功時用） |

### 出力先

- HTTPレスポンス（エラー時）
- brute テーブル

## 処理フロー

### 処理シーケンス

```
1. HTTPリクエスト受信
   └─ ミドルウェアチェーン内でbrute.*ミドルウェア実行
2. キー生成
   └─ IP + パス名 or ユーザー識別子
3. bruteテーブル検索
   └─ firstRequest, lastRequest, count取得
4. 閾値判定
   ├─ 超過: TooManyRequestsError返却
   └─ 未超過: countインクリメント、next()
5. 認証成功時（一部）
   └─ req.brute.reset()でカウンタリセット
```

### フローチャート

```mermaid
flowchart TD
    A[HTTPリクエスト] --> B[bruteミドルウェア]
    B --> C[キー生成]
    C --> D[bruteテーブル検索]
    D --> E{レコード存在?}
    E -->|No| F[新規レコード作成]
    E -->|Yes| G{lifetimeチェック}
    G -->|期限切れ| H[レコードリセット]
    G -->|有効| I{閾値超過?}
    H --> I
    F --> I
    I -->|Yes| J[TooManyRequestsError]
    I -->|No| K[countインクリメント]
    K --> L[bruteテーブル更新]
    L --> M[next()]
    M --> N{認証成功?}
    N -->|Yes| O[req.brute.reset]
    O --> P[bruteレコード削除]
    N -->|No| Q[エラー継続]
    J --> R[429応答]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-90-01 | フィボナッチ待機 | 失敗毎に待機時間がフィボナッチ数列で増加 | userLogin |
| BR-90-02 | 成功時リセット | 認証成功でカウンタリセット | userLogin, contentApiKey |
| BR-90-03 | 時間ベースリセット | lifetime経過でカウンタ自動リセット | 全エンドポイント |
| BR-90-04 | IP単位制限 | 同一IPからの試行を制限 | globalBlock, userLogin等 |
| BR-90-05 | ユーザー単位制限 | 同一ユーザー（email）への試行を制限 | userReset |
| BR-90-06 | Content API成功リセット | ステータス400未満でリセット | contentApiKey |

### 計算ロジック

待機時間計算: express-bruteのフィボナッチバックオフ
- minWait: 最小待機時間
- maxWait: 最大待機時間
- lifetime: カウンタ有効期間
- freeRetries: ブロック前の許容試行回数

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 試行記録 | brute | INSERT/UPDATE | 試行回数記録 |
| カウンタリセット | brute | DELETE | 成功時・期限切れ時 |
| 状態確認 | brute | SELECT | 現在の試行状態確認 |

### テーブル別操作詳細

#### brute

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | key | IP+パス or 識別子 | プライマリキー |
| INSERT/UPDATE | firstRequest | 初回リクエストタイムスタンプ | bigInteger |
| INSERT/UPDATE | lastRequest | 最終リクエストタイムスタンプ | bigInteger |
| INSERT/UPDATE | lifetime | カウンタ有効期間（ミリ秒） | bigInteger |
| INSERT/UPDATE | count | 試行回数 | integer |
| DELETE | key | 成功時・期限切れ時 | - |

### スキーマ定義

```javascript
brute: {
    key: {type: 'string', maxlength: 191, primary: true},
    firstRequest: {type: 'bigInteger'},
    lastRequest: {type: 'bigInteger'},
    lifetime: {type: 'bigInteger'},
    count: {type: 'integer'}
}
```

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 429 | TooManyRequestsError | 試行回数超過 | 待機後再試行 |
| - | InternalServerError | ストア操作エラー | ログ記録、継続 |

### エラーメッセージ

- userLogin: `Too many login attempts. Please wait X before trying again, or reset your password.`
- userReset: `Too many password reset attempts try again in X`
- globalBlock: `Too many attempts try again in X`
- membersAuth: `Too many sign-in attempts try again in X`
- contentApiKey: `Too many attempts.`

### リトライ仕様

nextValidRequestDateで示される時刻以降に再試行可能

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

brute-knexによる個別トランザクション

## パフォーマンス要件

- keyカラムにプライマリキーインデックス
- 高頻度アクセスに対応（認証前エンドポイント）

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

- Express trust proxyを有効化してreq.ipを信頼
- Content APIはMemoryStoreを使用（永続化不要）
- ユーザー列挙攻撃対策（membersAuthEnumeration）
- OTC検証の複数コード試行制限（otcVerificationEnumeration）

## 備考

- 設定はconfig.get('spam')で取得
- 各インスタンスはシングルトンパターンで管理
- reset()関数で全インスタンスをクリア可能（テスト用）

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | bruteテーブル定義（322-328行目） |

**読解のコツ**: key（プライマリキー）、firstRequest/lastRequest（タイムスタンプ）、lifetime、countの構造を確認。

#### Step 2: ミドルウェアエントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | brute.js | `ghost/core/core/server/web/shared/middleware/brute.js` | ミドルウェアエクスポート |

**主要処理フロー**:
- **12-18行目**: globalBlock - ルート+IP単位制限
- **34-40行目**: userLogin - IP単位ログイン制限
- **45-51行目**: userReset - ユーザー単位リセット制限
- **56-62行目**: sendVerificationCode - IP単位コード送信制限
- **67-73行目**: userVerification - IP単位検証制限
- **78-84行目**: privateBlog - プライベートブログ認証制限
- **90-106行目**: contentApiKey - Content API制限（成功時リセット）
- **111-121行目**: membersAuth - メンバー認証制限
- **127-128行目**: membersAuthEnumeration - 列挙攻撃対策
- **134-135行目**: otcVerificationEnumeration - OTC列挙対策
- **141-151行目**: otcVerification - OTC検証制限
- **158-164行目**: webmentionsLimiter - Webmentionスパム対策
- **171-177行目**: previewEmailLimiter - プレビューメール制限

#### Step 3: スパム対策サービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | spam-prevention.js | `ghost/core/core/server/web/shared/middleware/api/spam-prevention.js` | ExpressBruteインスタンス生成 |

**主要処理フロー**:
- **8行目**: spam設定読み込み
- **62行目**: spamConfigKeys定義（freeRetries, minWait, maxWait, lifetime）
- **86-113行目**: globalBlock - 50回/時間、1時間ロック
- **321-350行目**: userLogin - フィボナッチ待機
- **356-383行目**: userReset - 5回/時間
- **437-472行目**: privateBlog - 10回/時間
- **474-495行目**: contentApiKey - MemoryStore使用

#### Step 4: ルーティングでの使用を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | Admin APIルート |

**主要処理フロー**:
- **266-270行目**: /session POST - globalBlock + userLogin
- **272-273行目**: /session/verify - sendVerificationCode, userVerification
- **279-282行目**: /authentication/password_reset POST - globalReset + userReset
- **284行目**: /authentication/password_reset PUT - globalBlock

#### Step 5: セッションAPIでの統合を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | session.js | `ghost/core/core/server/api/endpoints/session.js` | セッションAPI |

**主要処理フロー**:
- **35-38行目**: req.brute.reset() - 認証成功時のカウンタリセット

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

```
Brute Middleware (brute.js)
    │
    ├─ globalBlock()
    │      └─ spamPrevention.globalBlock().getMiddleware()
    │             └─ ExpressBrute + BruteKnex
    │
    ├─ userLogin()
    │      └─ spamPrevention.userLogin().getMiddleware()
    │             └─ ExpressBrute + BruteKnex
    │             └─ attachResetToRequest: true → req.brute.reset()
    │
    ├─ userReset()
    │      └─ spamPrevention.userReset().getMiddleware()
    │             └─ key: email + 'reset'
    │
    ├─ contentApiKey()
    │      └─ spamPrevention.contentApiKey().getMiddleware()
    │             └─ ExpressBrute + MemoryStore
    │             └─ res.on('finish') → reset if status < 400
    │
    └─ membersAuth()
           └─ spamPrevention.membersAuth().getMiddleware()
                  └─ key: email + 'login'
```

### データフロー図

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

HTTP Request   ───▶ brute.userLogin()      ───▶ next() or 429
  (IP, path)        (ExpressBrute)

                           │
                           ▼
                    BruteKnex Store
                           │
                           ▼
                    brute テーブル
                    (key, count, etc)

Login Success  ───▶ req.brute.reset()      ───▶ DELETE from brute
                    (session.js:35)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | bruteテーブル定義 |
| brute.js | `ghost/core/core/server/web/shared/middleware/brute.js` | ミドルウェア | エクスポート |
| spam-prevention.js | `ghost/core/core/server/web/shared/middleware/api/spam-prevention.js` | ソース | 本体実装 |
| index.js | `ghost/core/core/server/web/shared/middleware/index.js` | ソース | ミドルウェアエクスポート |
| routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | ソース | Admin APIルート |
| session.js | `ghost/core/core/server/api/endpoints/session.js` | ソース | セッションAPI |
| middleware.js | `ghost/core/core/server/web/api/endpoints/content/middleware.js` | ソース | Content APIミドルウェア |
| app.js | `ghost/core/core/server/web/members/app.js` | ソース | メンバーAPI |
