レート制限(Rate Limiting)

レート制限は、APIの乱用・DoS攻撃・スクレイピングからシステムを保護するための基本的な防御策です。

アルゴリズムの比較

アルゴリズム特徴メリットデメリット
Fixed Window固定時間枠でカウント実装が簡単、メモリ効率が良いウィンドウ境界でバースト発生
Sliding Window Logリクエスト時刻を記録正確な制御メモリ消費大
Sliding Window Counter前後ウィンドウの加重平均バランスが良いやや複雑
Token Bucketトークンを消費してアクセスバーストを許容しつつ制限パラメータ調整が必要
Leaky Bucket一定速度でリクエスト処理出力レートが安定バースト対応が苦手

Express.js でのレート制限実装

JavaScript (Express)多層レート制限
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// グローバル制限: 全APIに適用
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100,
  standardHeaders: true,    // RateLimit-* ヘッダー返却
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

// 認証エンドポイント用: より厳しい制限
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // ログイン試行は15分で5回まで
  skipSuccessfulRequests: true, // 成功時はカウントしない
});

// 分散環境向け: Redis バックエンド
const distributedLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
  windowMs: 60 * 1000,
  max: 30,
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);

レスポンスヘッダー

HTTP Response HeadersRFC 6585 / draft-ietf-httpapi-ratelimit-headers
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1672531200
Retry-After: 30
レート制限のキー

IPアドレスだけでなく、APIキー・ユーザーID・エンドポイント単位など複合的なキーでレート制限を行うことで、正当なユーザーへの影響を最小限に抑えられます。

🔍 入力検証(Input Validation)

すべてのAPI入力は悪意あるデータを含む可能性があります。サーバーサイドで必ず検証してください。

検証の原則

Allowlist方式

許可する値のリストを定義。Denylist(禁止リスト)より安全。新しい攻撃パターンにも対応可能。

型・長さ・範囲

データ型、最大文字数、数値の上下限を厳密にチェック。バッファオーバーフロー対策の基本。

サニタイズ vs 拒否

不正入力は「修正」よりも「拒否」が安全。サニタイズは予期しない変換を生む可能性がある。

JSON Schema によるバリデーション

JavaScript (Express + Ajv)スキーマバリデーション
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);

// ユーザー作成APIのスキーマ
const createUserSchema = {
  type: 'object',
  required: ['name', 'email'],
  additionalProperties: false,
  properties: {
    name: {
      type: 'string',
      minLength: 1,
      maxLength: 100,
      pattern: '^[a-zA-Z0-9\\s\\-]+$', // 許可文字を限定
    },
    email: {
      type: 'string',
      format: 'email',
      maxLength: 254,
    },
    age: {
      type: 'integer',
      minimum: 0,
      maximum: 150,
    },
  },
};

// バリデーションミドルウェア
function validateBody(schema) {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors,
      });
    }
    next();
  };
}

app.post('/api/users', validateBody(createUserSchema), createUser);

よくある攻撃とバリデーション対策

攻撃対策
SQLインジェクションパラメータ化クエリ、ORM使用db.query('SELECT * FROM users WHERE id = ?', [id])
NoSQLインジェクション型チェック、$演算子のサニタイズ入力が文字列であることを確認(Object拒否)
XSS(APIレスポンス経由)Content-Type指定、エスケープContent-Type: application/json
パストラバーサル入力からパスセパレータを除去path.basename()で正規化
XXE(XML External Entity)外部エンティティ解決を無効化XMLパーサー設定で無効化

🌐 CORS(Cross-Origin Resource Sharing)

CORSはブラウザのSame-Originポリシーを制御する仕組みです。設定ミスは重大なセキュリティリスクになります。

危険な設定

Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true は同時に使えません。ワイルドカードの使用は公開APIのみに限定してください。

安全なCORS設定

JavaScript (Express)CORS設定
const cors = require('cors');

// 許可するオリジンを明示的に指定
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use(cors({
  origin(origin, callback) {
    // サーバー間通信(originなし)は許可
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,  // プリフライトキャッシュ: 24時間
}));

CORS チェックリスト

CORS ヘッダー一覧

ヘッダー用途推奨値
Access-Control-Allow-Origin許可するオリジン明示的なドメイン指定
Access-Control-Allow-Methods許可するHTTPメソッド必要最小限
Access-Control-Allow-Headers許可するリクエストヘッダー必要最小限
Access-Control-Allow-CredentialsCookie送信の許可true(認証必要時のみ)
Access-Control-Max-Ageプリフライトキャッシュ秒数86400(24時間)
Access-Control-Expose-HeadersJSから読めるレスポンスヘッダーRateLimit-*等 必要なもののみ

🤖 AI / LLM API レート制限

LLM APIは、レート制限に新たな考慮事項をもたらします。Token消費量、リクエストあたりのコスト、計算集約型の推論処理への対応が必要です。

従来のAPI vs. LLM API レート制限の比較

観点従来のAPILLM API
リクエストあたりのコスト低コスト、予測可能可変、Token数に応じて100倍以上になる場合あり
レート制限の単位時間枠あたりのリクエスト数TPM(Tokens per minute)+ RPM(Requests per minute)
悪用パターンスクレイピング、ブルートフォース、DDoSPrompt injection、リソース枯渇、denial-of-wallet
適合するアルゴリズム固定ウィンドウ / スライディングウィンドウToken bucket(Token数による重み付け)

Token対応レート制限(Python)

PythonTokenベースのレートリミッター
import tiktoken
import time
from collections import defaultdict

class TokenRateLimiter:
    """Rate limiter that counts tokens, not just requests."""

    def __init__(self, tokens_per_minute=100_000, requests_per_minute=60):
        self.tpm_limit = tokens_per_minute
        self.rpm_limit = requests_per_minute
        self.usage = defaultdict(lambda: {"tokens": [], "requests": []})
        self.encoder = tiktoken.encoding_for_model("gpt-4")

    def count_tokens(self, text: str) -> int:
        return len(self.encoder.encode(text))

    def check_limit(self, user_id: str, prompt: str) -> dict:
        now = time.time()
        window = now - 60  # 1分間のスライディングウィンドウ
        user = self.usage[user_id]

        # 期限切れエントリのクリーンアップ
        user["tokens"] = [(t, c) for t, c in user["tokens"] if t > window]
        user["requests"] = [t for t in user["requests"] if t > window]

        # RPMのチェック
        if len(user["requests"]) >= self.rpm_limit:
            return {"allowed": False, "reason": "RPM limit exceeded"}

        # TPMのチェック
        token_count = self.count_tokens(prompt)
        used_tokens = sum(c for _, c in user["tokens"])
        if used_tokens + token_count > self.tpm_limit:
            return {"allowed": False, "reason": "TPM limit exceeded"}

        # 使用量を記録
        user["tokens"].append((now, token_count))
        user["requests"].append(now)
        return {"allowed": True, "tokens_used": token_count}
OWASP リファレンス

関連: LLM10: モデルのサービス拒否ASI04: 連鎖的ハルシネーション攻撃

🛡 Prompt Injection と AI入力検証

Prompt injectionはLLMアプリケーションにおける最大のリスクです。入力検証、構造的分離、出力検証による多層防御を実施してください。

プロンプトのサニタイズ

ユーザー入力をプロンプトに含める前に、特殊Token、命令のようなパターン、制御文字を除去またはエスケープしてください。

構造的な入力分離

デリミタ、XMLタグ、またはメッセージロールの分離を使用して、システム命令とユーザー提供コンテンツを明確に隔離してください。

出力の検証

LLMの応答を期待されるスキーマに対して検証してください。レンダリング前に、データ漏洩、命令の追従、悪意あるコンテンツの有無を確認してください。

Prompt Injection の攻撃ベクトルと緩和策

攻撃対策
直接的Prompt Injection入力サニタイズ + 命令/データの分離「前の指示を無視して...」
間接的Prompt InjectionRAG結果のサニタイズ + カナリアToken取得ドキュメントに隠された悪意ある命令
コンテキストスタッフィングToken制限 + 入力の切り詰めコンテキストウィンドウを埋め尽くしてシステム命令を押し出す
パラメータ改ざんスキーマバリデーション + 型付きパラメータtemperature、max_tokens、モデルパラメータの操作

構造化された入力検証(Python / Pydantic)

PythonLLMリクエスト用 Pydantic バリデーション
from pydantic import BaseModel, Field, field_validator
import re

class LLMRequest(BaseModel):
    """Validated LLM request with prompt injection defenses."""
    user_message: str = Field(..., max_length=4000)
    max_tokens: int = Field(default=1000, ge=1, le=4096)
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)

    @field_validator("user_message")
    @classmethod
    def sanitize_prompt(cls, v: str) -> str:
        # 既知のインジェクションパターンをブロック
        patterns = [
            r"(?i)ignore\s+(previous|above|all)\s+(instructions?|prompts?)",
            r"(?i)you\s+are\s+now\s+",
            r"(?i)system\s*:\s*",
            r"(?i)\[INST\]|\[\/INST\]|<\|im_start\|>",
        ]
        for pattern in patterns:
            if re.search(pattern, v):
                raise ValueError("Input contains disallowed patterns")
        return v

# FastAPIでの使用例
@app.post("/api/chat")
async def chat(request: LLMRequest):
    # システム命令とユーザー入力を分離
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"<user_input>{request.user_message}</user_input>"},
    ]
    return await call_llm(messages, request.max_tokens, request.temperature)
OWASP リファレンス

関連: LLM01: Prompt InjectionLLM02: 安全でない出力処理ASI02: ツール結果を介したPrompt Injection

🔒 その他の推奨セキュリティヘッダー

HTTP Response Headers推奨設定
# コンテンツタイプの自動判別を無効化
X-Content-Type-Options: nosniff

# iframe埋め込みを禁止
X-Frame-Options: DENY

# HTTPS強制
Strict-Transport-Security: max-age=31536000; includeSubDomains

# CSP: APIはJSONのみ返すためscript実行を全て禁止
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

# Referrer情報の制限
Referrer-Policy: no-referrer

# ブラウザの機能制限
Permissions-Policy: geolocation=(), camera=(), microphone=()