連載の現在地:Phase 3「安全」(2 / 4本目) — 悪用や情報漏えいのリスクを潰す
1. 公開 → 2. 費用と安定 → 3. 安全 → 4. 品質 → 5. 自動化
こんな悩みの人へ
公開したAIチャットに、夜中に大量のリクエストが来ていた。問い合わせフォームにも、意味のない文章が毎日届く。とはいえ、画像認証(CAPTCHA)は利用者に面倒をかけるので、できれば入れたくない。そんな方に向けた記事です。
結論
Cloudflare Turnstileで自動アクセスを判定し、WAFのレート制限で短時間の大量リクエストを抑えます。 Turnstileは無料プランで利用でき、画像選択式のパズルを使いません。ただし、利用者の本人確認や、ボットをすべて排除する保証ではありません。ログインなしで公開するAI機能の悪用を減らす対策として組み合わせます。
ログイン必須でも、自動アカウント作成や認証済みアカウントの悪用は別に対策します。Turnstileを入れる場所は、登録・ログイン・AI呼び出しなど、実際の悪用経路に応じて決めます。
なぜAIのAPIは狙われるのか
AIのAPIが狙われやすいのは、使われるほど運営者にお金がかかるからです。普通のWebページと違い、AIの機能は1回ごとに利用料が発生します。誰でも叩けるAPIは、他人の財布でAIを使える入口になってしまいます。
画面からしか使えないように見えても、ブラウザからの呼び出しはまねできます。通信の中身を見れば、同じリクエストをプログラムから送れるからです。かといって、IPアドレスだけでは止めきれません。攻撃は多数のIPアドレスから来るため、特定のアドレスをブロックしても追いつかないのです。
そのため、自動アクセスの判定と、IPや利用者ごとの回数制限を組み合わせます。
解決策の比較
方法 | 費用 | 手間 | 向いている状況 |
|---|---|---|---|
Turnstile + WAFのレート制限 | Turnstileは無料プランあり | 中くらい | ログインなしで使える機能を公開している |
Google reCAPTCHAなど | 無料枠あり | 中くらい | すでに導入済みで、乗り換える理由がない |
ログイン必須にする | 認証の実装 | 多い | もともと会員向けのサービス |
アプリ内での回数制限のみ | 開発の工数 | 中くらい | 利用者が限られ、攻撃される心配が小さい |
どれを選ぶか
誰でも使える無料のAI機能があるなら、Turnstileとレート制限の組み合わせが基本です。会員向けで、ユーザーごとの上限がすでにある場合も、登録やログインの保護を確認し、必要な場所にTurnstileを追加します。費用の上限もあわせて設定しておきましょう(「LLM APIの請求額が読めない」)。すでに別のCAPTCHAを導入していて、問題が起きていないなら、乗り換えは急がなくて構いません。
手順
前提:APIがCloudflare Workers上にある例です。ほかのサーバーでも、検証の部分は同じです。
この手順で作る構成は、次のとおりです。LLMのAPIを呼ぶのは、すべての確認を通ったリクエストだけです。
[ブラウザ] Turnstileのウィジェットがトークンを発行
| 質問 + cf-turnstile-response
v
[WAFのレート制限] 同じIPからの、短時間の大量リクエストを止める
|
v
[Workers] Siteverifyでトークンを検証(hostname・actionも照合)
| 合格したときだけ
v
[LLM API]- Cloudflareのダッシュボードで「Turnstile」を開き、ウィジェットを追加します。使うドメインを登録すると、サイトキー(公開してよい)とシークレットキー(公開しない)が発行されます。
- 画面側にウィジェットを置きます。フォームを送信すると、
cf-turnstile-responseという名前でトークンが一緒に送られます。
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="ask" method="POST" action="/api/ask">
<textarea name="question"></textarea>
<div class="cf-turnstile" data-sitekey="あなたのサイトキー" data-action="ask"></div>
<button type="submit">送信</button>
</form>- シークレットキーを、Workersのシークレットに登録します。
npx wrangler secret put TURNSTILE_SECRET- サーバー側で、必ずトークンを検証します。 画面にウィジェットを置くだけでは、守れていません。
async function verifyTurnstile(
token: string, ip: string | null, secret: string, expectedHostname: string,
) {
if (!token || token.length > 2048) return false;
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret, response: token, remoteip: ip ?? undefined }),
});
if (!res.ok) return false;
const data = (await res.json()) as {
success: boolean; hostname?: string; action?: string;
};
return data.success && data.hostname === expectedHostname && data.action === "ask";
}
export default {
async fetch(request: Request, env: { TURNSTILE_SECRET: string }) {
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const form = await request.formData();
const token = String(form.get("cf-turnstile-response") ?? "");
const ip = request.headers.get("CF-Connecting-IP");
const expectedHostname = new URL(request.url).hostname;
if (!(await verifyTurnstile(token, ip, env.TURNSTILE_SECRET, expectedHostname))) {
return new Response("Forbidden", { status: 403 });
}
// ここで初めてLLMのAPIを呼ぶ
return new Response("OK");
},
};この例はフォームとAPIが同じホスト名にある構成です。別のホスト名を使う場合は、フォームを置いたホスト名をサーバー側の設定で指定します。Siteverifyの成功だけでなく、hostname と用途を示す action も照合します。
hostname と action まで見るのは、別のサイトや別のフォームで取得したトークンを流用されないようにするためです。token.length > 2048 で先に弾いているのは、Siteverifyが受け付けるトークンの長さの上限に合わせ、明らかに不正な値で検証APIを呼ばないためです。
- Cloudflareで管理している対象ドメインの「Security rules」で「Create rule」→「Rate limiting rules」を選び、APIのパス(例:
/api/ask)にレート制限ルールを追加します。無料プランは1ルール、カウント単位はIP、集計期間とブロック期間はそれぞれ10秒です。回数は普通の利用者の使い方を見て決めます。このWAF設定は対象ドメインを経由する通信に適用されるため、workers.devなど別の公開URLからAPIを呼べる構成では、その入口も保護します。プラン別の設定範囲を参照してください。
更新日:2026-11-03
つまずきやすい点
Turnstileのトークンは、5分(300秒)で切れ、1回しか使えません。画面を開いたまましばらく放置してから送信すると、検証に失敗します。失敗したら、ウィジェットを再読み込みして(turnstile.reset)、トークンを取り直します。同じ理由で、同じトークンを再送しても失敗します。送信ボタンの二度押しや、通信エラー時の自動再送で起きがちなので注意してください。
シークレットキーは、画面側に書いてはいけません。検証は、サーバーからだけ行います。
無料プランには上限があります。ウィジェットは1アカウントにつき20個まで、1つのウィジェットに登録できるホスト名は10個までです。検証の回数そのものに上限はありません。
また、WAFは厳密な利用回数の台帳ではありません。カウンターへの反映には遅延があり、設定した回数を超えた通信が届く場合があります。利用者ごとの厳密な回数制限は、アプリ側でも実装してください。


次に出やすい悩み
入口の悪用を抑えられたら、次は、すり抜けた分の費用と、ログイン済みの利用者ごとの上限を固めます。すり抜けたリクエストの費用も金額で止めたいなら「LLM APIの請求額が読めない」を、ログイン済みのユーザーごとに厳密な回数制限をかけたいなら「ユーザーごとにAIの利用量を制限したい」を参考にしてください(どちらもPhase 2の記事です)。画面側を見直すなかで、APIキーの置き場所も気になったら、「APIキーの漏えいが怖い」で整理しています。
「夜中のアクセスが攻撃なのか、普通の利用なのか判断がつかない」という段階でもご相談ください。ボット対策やアクセス制限の設計はCloudflareインフラ構築をご覧ください。
よくある質問
Q. Turnstileのウィジェットを画面に置くだけで対策できますか?
サーバー側でSiteverify APIを呼び、トークンを検証する必要があります。成功の判定に加え、本文の例ではhostnameとactionも照合し、確認を通ったリクエストだけでLLMを呼びます。シークレットキーは画面側へ渡しません。サーバー側検証の仕様も確認してください。
Q. Turnstileを入れれば、ログインや利用者ごとの上限は不要ですか?
Turnstileは利用者の本人確認や、利用権限の管理を代替しません。会員向けの機能では認証・認可を行い、利用者ごとの回数や金額の上限も設けます。登録、ログイン、AI呼び出しなど、悪用されている入口に応じて対策を組み合わせます。
Q. WAFのレート制限だけで、利用回数を正確に数えられますか?
厳密な利用回数の台帳としては扱わないでください。カウンターの反映に遅れが生じ、設定した回数を超える通信が届く場合があります。料金プランの回数制限など、正確な判定が必要な用途は、アプリ側でも認証した利用者ごとに管理します。
Q. トークンが期限切れになったり、送信をやり直したりするときは?
Turnstileのトークンは発行から5分間有効で、検証に使えるのは1回です。期限切れや再利用で失敗した場合は、ウィジェットをリセットして新しいトークンを取得します。送信ボタンの二度押しや通信エラー時にも、同じトークンで無制限に再送しない設計にします。
執筆者情報
IT顧問、AI・業務アプリ開発、SESを通じて、企業のIT活用を支援しています。代表の藤澤 昌隆は、2007年からシステム開発・運用に携わり、アクセンチュアでの業務ITコンサルティングを経て、2019年にパブロフ株式会社を設立しました。
AI活用の支援事例として、営業工程を支える7つの業務別AIアプリの構築を公開しています。アプリの開発に加え、入力・出力の形式、評価基準、テストケースの整備にも取り組んでいます。
記事の内容に関するご質問や誤りのご指摘は、お問い合わせ窓口へお寄せください。
