mcpField
C
cloudflare-turnstile

cloudflare-turnstile

github.com/jezwebセキュリティ1.1k347 回インストール3 週間前に更新
リポジトリ

cloudflare-turnstile について

cloudflare-turnstile は、Cloudflare Turnstile の導入方法をまとめたスキルです。フロントエンドのウィジェットとサーバー側のトークン検証を扱い、ウェブ開発者向けに統合、テスト、セキュリティ要件、トラブルシューティングを説明します。

主な機能

  1. 01暗黙的・明示的ウィジェットの設定
  2. 02React への Turnstile の統合
  3. 03Siteverify によるサーバー側トークン検証
  4. 04テストキーと検証パターンの利用
  5. 05一般的なエラーや CSP の問題の診断

活用シーン

  1. 01ウェブサイトのフォームへのボット対策追加
  2. 02API ルートでの Turnstile トークン検証
  3. 03検証成功・失敗シナリオのテスト
  4. 04ウィジェット読み込みや検証問題の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-turnstile

インストール後、Claude Code で/cloudflare-turnstileを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
error-codes.md
example-reference.md
react-integration.md
testing-guide.md
widget-configs.md
rules/
cloudflare-turnstile.md
scripts/
check-csp.sh
example-script.sh
templates/
turnstile-hono-route.ts
turnstile-react-component.tsx
turnstile-server-validation.ts
turnstile-test-config.ts
turnstile-widget-explicit.ts
turnstile-widget-implicit.html

詳細

Cloudflare Turnstile

ステータス:本番環境で利用可能 ✅ 最終更新日:2026-01-21 依存関係:なし(オプション:React 用の @marsidev/react-turnstile) 最新バージョン:@marsidev/[email protected]、[email protected]

最近の更新(2025):

  • 2025 年 12 月:@marsidev/react-turnstile v1.4.1 で、スクリプト読み込み時の競合状態を修正
  • 2025 年 8 月:v1.3.0 で、React のクロージャに関する問題に対応する rerenderOnCallbackChange プロパティを追加
  • 2025 年 3 月:TopN 統計(7 つのディメンション:ホスト名、ブラウザー、国、ユーザーエージェント、ASN、OS、送信元 IP)を含む Turnstile Analytics のアップグレード、異常検知、ボットの挙動監視を強化
  • 2025 年 1 月:remoteip の検証を一時的に強制(解決済み。ただし、正しい IP の渡し方が重要であることを示しています)
  • 2025 年:WCAG 2.1 AA 準拠、Free プラン(ウィジェット 20 個、分析データ 7 日分)、Enterprise 機能(ウィジェット数無制限、エフェメラル ID、すべてのホスト名に対応、分析データ 30 日分、オフラベルブランディング)

クイックスタート(5 分)

# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
#    Copy sitekey (public) and secret key (private)

# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Submit</button>
</form>

# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')

const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP'))  // REQUIRED - see Critical Rules

const result = await fetch(
  'https://challenges.cloudflare.com/turnstile/v0/siteverify',
  { method: 'POST', body: verifyFormData }
)

const outcome = await result.json()
if (!outcome.success) return new Response('Invalid', { status: 401 })

重要:

  • トークンの有効期限は 5 分で、1 回のみ使用できます
  • 必ずサーバー側で検証してください(Siteverify API が必須です)
  • api.js をプロキシ/キャッシュしないでください(Cloudflare CDN から読み込む必要があります)
  • 開発/ステージング/本番環境では、異なるウィジェットを使用してください

レンダリングモード

暗黙的(ページ読み込み時に自動レンダリング):

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess"></div>

明示的(SPA 向けのプログラムによる制御):

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId)   // Reset widget
turnstile.getResponse(widgetId)  // Get token

React(@marsidev/react-turnstile を使用):

import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />

重要なルール

必ず行うこと

✅ Siteverify API を呼び出す - サーバー側での検証は必須です ✅ HTTPS を使用する - HTTP 経由で検証しないでください ✅ シークレットキーを保護する - フロントエンドのコードで公開しないでください ✅ トークンの有効期限を処理する - トークンは 5 分後に期限切れになります ✅ エラーコールバックを実装する - 失敗を適切に処理してください ✅ テストにはダミーキーを使用する - テスト用 sitekey:1x00000000000000000000AA ✅ 妥当なタイムアウトを設定する - 検証を無期限に待たないでください ✅ action/hostname を検証する - 指定されている場合は追加フィールドを確認してください ✅ キーを定期的にローテーションする - ダッシュボードまたは API を使用してシークレットをローテーションしてください ✅ 分析データを監視する - 解決率と失敗を追跡してください ✅ クライアント IP を必ず Siteverify に渡す - CF-Connecting-IP ヘッダー(Workers)または X-Forwarded-For(Node.js)を使用してください。Cloudflare は 2025 年 1 月に remoteip の厳格な検証を一時的に強制し、正しい IP を渡していないサイトで広範囲に障害が発生しました

絶対に行わないこと

❌ サーバー側の検証を省略する - クライアント側だけの検証はセキュリティ上の脆弱性になります ❌ api.js スクリプトをプロキシする - Cloudflare CDN から読み込む必要があります ❌ トークンを再利用する - 各トークンは 1 回のみ使用できます ❌ GET リクエストを使用する - Siteverify が受け付けるのは POST のみです ❌ シークレットキーを公開する - シークレットはバックエンド環境内だけに保管してください ❌ クライアント側の検証を信用する - トークンは偽造される可能性があります ❌ api.js をキャッシュする - 将来の更新によって統合が機能しなくなる可能性があります ❌ テストで本番環境のキーを使用する - 代わりにダミーキーを使用してください ❌ エラーコールバックを無視する - 必ず失敗を処理してください


既知の問題の防止

このスキルは、文書化された 15 件の問題を防止します:

問題 #1:サーバー側検証の欠落

エラー:Turnstile Analytics ダッシュボードでトークン検証がゼロと表示される 出典:https://developers.cloudflare.com/turnstile/get-started/ 発生理由:開発者がクライアント側のウィジェットのみを実装し、Siteverify の呼び出しを省略するため 防止策:すべてのテンプレートに、Siteverify API による必須のサーバー側検証が含まれています

問題 #2:トークンの有効期限(5 分)

エラー:有効なトークンでも、送信が遅れると success: false になる 出典:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 発生理由:トークンは生成から 300 秒後に期限切れになるため 防止策:テンプレートには TTL が記載されており、期限切れ時のトークン更新が実装されています

問題 #3:シークレットキーがフロントエンドで露出する

エラー:セキュリティの回避。攻撃者が自分のトークンを検証できてしまいます 出典:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 発生する理由:シークレットキーが JavaScript にハードコードされている、またはソースコードで確認できる 防止策:すべてのテンプレートで、環境変数を使ったバックエンドのみでの検証を示しています

問題 #4:Siteverify への GET リクエスト

エラー:API が 405 Method Not Allowed を返します 出典:https://developers.cloudflare.com/turnstile/migration/recaptcha 発生する理由:reCAPTCHA は GET をサポートしますが、Turnstile では POST が必要です 防止策:テンプレートでは、FormData または JSON ボディを使った POST を使用しています

問題 #5:コンテンツセキュリティポリシーによるブロック

エラー:エラー 200500 — 「Loading error: The iframe could not be loaded」 出典:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 発生する理由:CSP が challenges.cloudflare.com の iframe をブロックします 防止策:スキルには CSP 設定の参考情報と check-csp.sh スクリプトが含まれています

問題 #6:ウィジェットのクラッシュ(エラー 300030)

エラー:正規のユーザーに対して一般的なクライアント実行エラーが発生します 出典:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 発生する理由:不明です。Cloudflare 側の問題と思われます(2025 年) 防止策:テンプレートでは、エラーコールバック、再試行ロジック、フォールバック処理を実装しています

問題 #7:設定エラー(エラー 600010)

エラー:ウィジェットで「configuration error」が発生します 出典:https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 発生する理由:ウィジェットの設定にホスト名がない、または削除されています 防止策:テンプレートでは、ホスト名の許可リスト要件と確認手順を説明しています

問題 #8:Safari 18/macOS 15 の「IP アドレスを非表示にする」問題

エラー:Safari の「IP アドレスを非表示にする」が有効な場合、エラー 300010 が発生します 出典:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 発生する理由:プライバシー設定がチャレンジのシグナルに干渉します 防止策:エラー処理の参考情報で、Safari の回避策(「IP アドレスを非表示にする」を無効にする)を説明しています

問題 #9:Brave ブラウザーで紙吹雪のアニメーションが失敗する

エラー:成功アニメーション中に検証が失敗します 出典:https://github.com/brave/brave-browser/issues/45608(2025 年 4 月) 発生する理由:Brave のシールドがアニメーションのスクリプトをブロックします 防止策:テンプレートでは、アニメーションの完了前に成功を処理します

問題 #10:Next.js と Jest の非互換性

エラー:@marsidev/react-turnstile により Jest テストが失敗します 出典:https://github.com/marsidev/react-turnstile/issues/112(2025 年 10 月) 発生する理由:Jest でのモジュール解決に関する問題です 防止策:テストガイドには、Jest のモックパターンとダミーの sitekey の使用方法が含まれています

問題 #11:localhost が許可リストにない

エラー:エラー 110200 — 「Unknown domain: Domain not allowed」 出典:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 発生する理由:許可リストに localhost がないまま、開発環境で本番用ウィジェットを使用しています 防止策:テンプレートでは、開発時にダミーのテストキーを使用し、localhost を許可リストに追加する要件を説明しています

問題 #12:トークンの再利用を試みる

エラー:success: false と「token already spent」エラーが発生します 出典:https://developers.cloudflare.com/turnstile/troubleshooting/testing 発生する理由:各トークンは一度しか検証できません。Turnstile のトークンは使い捨てです。検証後(成功または失敗を問わず)、トークンは消費され、再検証できません。後続の送信で新しいトークンを生成するには、開発者が明示的に turnstile.reset() を呼び出す必要があります。 防止策:テンプレートでは、使い捨ての制約とトークンを更新するパターンを説明しています

// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)

async function handleSubmit(e) {
  e.preventDefault()
  const token = formData.get('cf-turnstile-response')

  const result = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ token })
  })

  // Reset widget regardless of success/failure
  // Token is consumed either way
  if (turnstileRef.current) {
    turnstile.reset(turnstileRef.current)
  }
}

<Turnstile
  ref={turnstileRef}
  siteKey={TURNSTILE_SITE_KEY}
  onSuccess={setToken}
/>
Issue #13:エラー 106010 - Chrome/Edge で初回読み込みに失敗

エラー:Chrome/Edge ブラウザーでウィジェットを初めて読み込む際に、106010 - 「一般的なパラメーターエラー」が発生します 参照元:Cloudflare エラーコード、コミュニティの報告 発生する理由:Chrome および Edge で、ページの初回読み込み時に影響するブラウザー固有の問題です。コンソールには https://challenges.cloudflare.com/cdn-cgi/challenge-platform への 400 エラーが表示されます。Firefox では発生しません。ページを再読み込みすると正常に動作します。 予防策:エラーコールバックに自動再試行ロジックを実装します

turnstile.render('#container', {
  sitekey: SITE_KEY,
  retry: 'auto',
  'retry-interval': 8000,
  'error-callback': (errorCode) => {
    if (errorCode === '106010') {
      console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
      // Auto-retry will handle it
    }
  }
})

回避策:ページを再読み込みすると、ウィジェットは正常に動作します。自動再試行の設定で、ほとんどの場合は解決します。ブラウザー拡張機能が原因でないことを確認するには、シークレットモードでテストします。Cloudflare Turnstile のエンドポイントが許可されていることを確認するため、CSP ルールを確認します。

Issue #14:複数ウィジェットの表示ステータスが更新されない(コミュニティ提供)

エラー:トークンの生成に成功しても、ウィジェットに「保留中...」というステータスが表示されます 参照元:GitHub Issue #119 発生する理由:1 つのページで複数の <Turnstile/> コンポーネントをレンダリングする際に発生する、CSS の再描画に関する問題です。フル HD のデスクトップ画面でのみ再現します。トークンは正常に生成されており(検証は機能します)が、表示上のステータスが更新されません。ウィジェットにマウスカーソルを合わせると再描画が行われ、正しいステータスが表示されます。 予防策:成功コールバックで CSS の再描画を強制します

<Turnstile
  siteKey={KEY}
  onSuccess={(token) => {
    setToken(token)
    // Force repaint by toggling display
    const widget = document.querySelector('.cf-turnstile')
    if (widget) {
      widget.style.display = 'none'
      setTimeout(() => widget.style.display = 'block', 0)
    }
  }}
/>

注:これは表示上のみの問題であり、検証の失敗ではありません。トークンは正しく生成され、機能します。

Issue #15:@marsidev/react-turnstile の Jest 互換性(2025 年 12 月更新)

エラー:@marsidev/react-turnstile のインポート時に Jest encountered an unexpected token が発生します 参照元:GitHub Issue #114、GitHub Issue #112 発生する理由:Jest 30.2.0(2025 年 12 月時点の最新版)で、ESM モジュールの解決に関する問題が発生します。Issue #112 はメンテナーによって「対応予定なし」としてクローズされました。Jest ユーザーは身動きが取れない状態ですが、Vitest への移行で解決できます。 予防策:Jest のセットアップで Turnstile コンポーネントをモックするか、Vitest に移行します

// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: () => <div data-testid="turnstile-mock" />,
}))

// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
  transformIgnorePatterns: [
    'node_modules/(?!(@marsidev/react-turnstile)/)'
  ]
}

// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking

ステータス:メンテナーはこの Issue を「対応予定なし」としてクローズしました。新しいプロジェクトでは Vitest への移行をおすすめします。

設定

wrangler.jsonc:

{
  "vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
  "secrets": ["TURNSTILE_SECRET_KEY"]  // Run: wrangler secret put TURNSTILE_SECRET_KEY
}

必須の CSP:

<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-src 'self' https://challenges.cloudflare.com;
">

よく使われるパターン

パターン 1:Hono + Cloudflare Workers
import { Hono } from 'hono'

type Bindings = {
  TURNSTILE_SECRET_KEY: string
  TURNSTILE_SITE_KEY: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.post('/api/login', async (c) => {
  const body = await c.req.formData()
  const token = body.get('cf-turnstile-response')

  if (!token) {
    return c.text('Missing Turnstile token', 400)
  }

  // Validate token
  const verifyFormData = new FormData()
  verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
  verifyFormData.append('response', token.toString())
  verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')  // CRITICAL - always pass client IP

  const verifyResult = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: verifyFormData,
    }
  )

  const outcome = await verifyResult.json<{ success: boolean }>()

  if (!outcome.success) {
    return c.text('Invalid Turnstile token', 401)
  }

  // Process login
  return c.json({ message: 'Login successful' })
})

export default app

使用場面:Hono フレームワークを使用する Cloudflare Workers の API ルート

パターン 2:React + Next.js App Router
'use client'

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export function ContactForm() {
  const [token, setToken] = useState<string>()
  const [error, setError] = useState<string>()

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()

    if (!token) {
      setError('Please complete the challenge')
      return
    }

    const formData = new FormData(e.currentTarget)
    formData.append('cf-turnstile-response', token)

    const response = await fetch('/api/contact', {
      method: 'POST',
      body: formData,
    })

    if (!response.ok) {
      setError('Submission failed')
      return
    }

    // Success
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
        onError={() => setError('Challenge failed')}
        onExpire={() => setToken(undefined)}
      />

      {error && <div className="error">{error}</div>}

      <button type="submit" disabled={!token}>
        Submit
      </button>
    </form>
  )
}

使用場面:React フックを使用する Next.js のクライアント側フォーム


テストキー

ダミーサイトキー(クライアント):

  • 常に成功:1x00000000000000000000AA
  • 常にブロック:2x00000000000000000000AB
  • インタラクティブを強制:3x00000000000000000000FF

ダミーシークレットキー(サーバー):

  • 常に成功:1x0000000000000000000000000000000AA
  • 常に失敗:2x0000000000000000000000000000000AA
  • トークンは使用済み:3x0000000000000000000000000000000AA

同梱リソース

スクリプト: check-csp.sh - CSP で Turnstile が許可されていることを確認します

参照資料:

  • widget-configs.md - すべての設定オプション
  • error-codes.md - エラーコードのトラブルシューティング(100*/200*/300*/400*/600*)
  • testing-guide.md - テスト戦略、ダミーキー
  • react-integration.md - React/Next.js のパターン

テンプレート: Hono、React、暗黙的/明示的なレンダリング、検証の完全なサンプル


高度な機能

事前クリアランス(SPA): ページ遷移後も維持される Cookie を発行します

turnstile.render('#container', {
  sitekey: SITE_KEY,
  callback: async (token) => {
    await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
  }
})

カスタムアクションとデータ: チャレンジの種類を追跡し、カスタムデータを渡します(最大 255 文字)

turnstile.render('#container', {
  action: 'login',  // Track in analytics
  cdata: JSON.stringify({ userId: '123' }),  // Custom payload
})

エラー処理: 信頼性を高めるために retry: 'auto' と error-callback を使用します

turnstile.render('#container', {
  retry: 'auto',
  'retry-interval': 8000,  // ms between retries
  'error-callback': (error) => { /* handle or show fallback */ }
})

依存関係

必須: なし(CDN から読み込みます) React: @marsidev/[email protected](Cloudflare 推奨)、[email protected] その他: vue-turnstile、ngx-turnstile、svelte-turnstile、@nuxtjs/turnstile


公式ドキュメント


トラブルシューティング

問題:エラー 110200 - 「Unknown domain」

解決策:Cloudflare ダッシュボードで、ウィジェットの許可ドメインに使用するドメイン(開発用の localhost を含む)を追加します。ローカル開発では、代わりにダミーのテスト sitekey 1x00000000000000000000AA を使用します。

問題:エラー 300030 - 正規ユーザーに対してウィジェットがクラッシュする

解決策:再試行ロジックを含むエラーコールバックを実装します。これは Cloudflare 側で既知の問題です(2025 年)。再試行に失敗した場合は、別の検証方法にフォールバックします。

問題:トークンが常に success: false を返す

解決策:

  1. トークンの有効期限が切れていないことを確認します(TTL は 5 分です)
  2. シークレットキーが正しいことを確認します
  3. トークンが以前に検証されていないことを確認します(使い捨てです)
  4. ホスト名がウィジェットの設定と一致することを確認します
問題:CSP が iframe をブロックする(エラー 200500)

解決策:CSP ディレクティブを追加します:

<meta http-equiv="Content-Security-Policy" content="
  frame-src https://challenges.cloudflare.com;
  script-src https://challenges.cloudflare.com;
">
問題:Safari 18 の「IP アドレスを非公開にする」によってエラー 300010 が発生する

解決策:エラーメッセージに、Safari の「IP アドレスを非公開にする」設定を無効にするようユーザーに案内する内容を記載します(Safari → 設定 → プライバシー → IP アドレスを非公開にする → オフ)。

問題:Next.js + Jest のテストが @marsidev/react-turnstile で失敗する

解決策:Jest のセットアップで Turnstile コンポーネントをモックするか、ESM モジュールを正しく処理できる Vitest に移行します:

// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: () => <div data-testid="turnstile-mock" />,
}))

// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required

注記:この問題は Jest 30.2.0 でも解消されていません(2025 年 12 月)。メンテナーは「対応予定なし」としてクローズしました。詳細については Issue #15 を参照してください。


防止できるエラー: ドキュメント化された 15 件の問題(Safari 18 の「IP アドレスを非公開にする」、Brave の紙吹雪、Next.js Jest、CSP によるブロック、トークンの再利用、期限切れ、ホスト名の許可リスト、ウィジェットのクラッシュ 300030、設定エラー 600010、検証の欠落、GET リクエスト、シークレットの露出、Chrome/Edge 106010、複数ウィジェットのレンダリング、トークン再生成のパターン)

コメント