【実務・中級編】 モデル拒否サービス(DoS)攻撃の仕組みとレート制限の実装 – ガバナンス・リスク管理 & 生成AIセキュリティ防御ガイド

AI時代の「自滅」を防ぐ:モデルDoS攻撃からインフラを守る鉄則

現場で死線をくぐり抜けてきた諸君、お疲れ様。
最近、LLMを組み込んだアプリケーションをローンチした後に、「急にAPIのレスポンスが極端に遅くなった」「クラウドの利用料が予算の5倍になった」という泣き言をよく耳にする。

そのほとんどが、悪意あるユーザーによる「モデルDoS攻撃(Model DoS)」だ。

従来のDoS攻撃がネットワーク帯域を狙うのに対し、モデルDoSは「GPUの演算コスト」を食いつぶす。1プロンプトで数千円のコストがかかるLLMにおいて、これを放置するのは「財布の鍵を開けて通りすがりに金を配っている」のと同義だ。今日は、この泥臭いインシデントを未然に防ぐための防衛術を伝授する。

—

1. なぜ「モデルDoS」は従来の防御をすり抜けるのか

通常のWeb攻撃であれば、WAFがIP単位で接続数を制限すれば終わりだ。しかし、モデルDoSは違う。攻撃者は、以下のような「高負荷プロンプト」を巧妙に送り込んでくる。

  • 極端なトークン長: コンテキストウィンドウ上限ギリギリのゴミデータを送り込み、アテンション機構の計算量を爆発させる。
  • 再帰的・複雑な推論要求: 「この論理パズルを100ステップで解け」といった、GPUを長時間占有するタスク。
  • 並列リクエスト: 1つのIPから同時多発的にリクエストを飛ばし、モデルの推論キューを埋め尽くす。

これらは正常なリクエストと見分けがつきにくく、WAFの単純なレート制限だけでは「計算リソースの枯渇」までは防げない。「トークン数」「ユーザーの行動スコア」「キューの滞留時間」、この3層で防壁を築く必要がある。

—

2. 防御の要:Pythonによるトークン制限の実装例

まず、最も単純だが効果的なのが「入力トークン数の制限」だ。リクエストがモデルに到達する前に、トークン数をカウントして遮断する。

import tiktoken
from fastapi import HTTPException, Request

# モデルごとのトークン制限設定(コストとGPU負荷を考慮)
MAX_TOKENS = 4000 
encoding = tiktoken.get_encoding("cl100k_base")

async def validate_token_limit(request: Request, body: str):
    # プロンプトのトークン数を計算
    tokens = len(encoding.encode(body))
    
    # トークン数が閾値を超えていれば拒否
    if tokens > MAX_TOKENS:
        # 413 Payload Too Large を返すのが適切
        raise HTTPException(
            status_code=413, 
            detail=f"Token limit exceeded. Your prompt: {tokens}, Limit: {MAX_TOKENS}"
        )
    return True

—

3. レート制限の極意:ユーザー単位の「バケット制御」

IP制限だけでは、NAT配下の正当なユーザーを誤BANしてしまう。必ずログインユーザーID(またはAPIキー)単位で管理すること。

Redisを用いたトークンバケットアルゴリズムの実装が、今の現場ではデファクトスタンダードだ。

import redis
import time

# Redis接続(インフラチームと相談し、レイテンシの低いものを選ぶこと)
r = redis.Redis(host='localhost', port=6379, db=0)

def is_rate_limited(user_id: str, limit: int = 5, window: int = 60):
    """
    user_idごとに、window秒間にlimit回までのリクエストを許可する
    """
    key = f"rate_limit:{user_id}"
    count = r.get(key)
    
    if count and int(count) >= limit:
        return True # 制限超過
    
    # カウンタをインクリメントし、有効期限を設定
    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    pipe.execute()
    return False

—

4. インフラ層での防御:Nginxによる保護

アプリケーションコードだけで防御するのは「甘え」だ。万が一コードが落ちても、入り口で弾く設計にしておく。Nginxの limit_req モジュールは、特定のパス(/api/chatなど)に対して強力な防壁となる。

# nginx.conf の設定例
# 1秒間に1リクエストを許容し、バーストは5まで。user_idをキーにする
limit_req_zone $http_x_user_id zone=ai_limit:10m rate=1r/s;

server {
    location /api/chat {
        # 制限超過時は503を返す
        limit_req zone=ai_limit burst=5 nodelay;
        proxy_pass http://backend_cluster;
    }
}

—

5. 最後に:セキュリティは「多層」であるべき

技術的な実装を紹介したが、最後に一つだけ忠告しておく。セキュリティに「魔法の杖」はない。

  • モニタリング: DatadogやPrometheusで「推論にかかった時間(Latency)」と「トークン消費量」を可視化せよ。異常なスパイクは、攻撃の予兆だ。
  • キューイング: モデルの推論を同期処理にせず、一旦メッセージキュー(RabbitMQ/SQS)に逃がすアーキテクチャに切り替えることも検討しろ。これにより、スパイクが来てもアプリ全体が落ちるのを防げる。

「動くコード」を書くことはエンジニアの基礎だが、「攻撃されても耐えうるアーキテクチャ」を設計するのがプロの仕事だ。今日紹介したコードはそのままプロダクションに投入できるレベルのものだ。さあ、今すぐ君のシステムの監視ログを確認し、防御層が不足していないか見直してほしい。

健闘を祈る。

コメント

タイトルとURLをコピーしました