サイトが Cloudflare の背後にある場合、保護したいルートに置いた Worker が、リクエストがオリジンに届く前に Bot Protection に問い合わせることができます。アプリケーションの変更は一切不要です。Worker は Cloudflare のエッジで動き、同じく Cloudflare の背後にある korzen.dev の近くで実行されます。静的サイトでは、私たち自身のブログと同様に、Cloudflare Pages Function で 1 つの操作に対して同じことができます。
タグ
訪問者が送信する時点で _kz Cookie が存在するよう、タグは引き続きページの <head> に置きます。
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Worker
クライアントは依存関係のない 1 ファイルです。/v1/decide に問い合わせ、設定した timeout で打ち切り、エラーのときは必ず訪問者を通します。Worker の隣にダウンロードし、commit する前に中身を読んでください。
curl -o src/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js
createWorker だけで Worker が完成します。ルート上のすべてのリクエストをお客様の timeout で問い合わせ、設定は Worker の環境から読み取ります。
// src/index.js: a Worker on the routes Bot Protection judges, in front of the cache and your origin. // wrangler.toml: // routes = [ // { pattern = "tickets.example/api/resale/*", zone_name = "tickets.example" }, // { pattern = "tickets.example/orders/*", zone_name = "tickets.example" }, // ] // [vars] // KORZEN_URL = "https://korzen.dev" // KORZEN_TIMEOUT_MS = "200" // KORZEN_REQUIRE_TOKEN = "/orders/*" // The key: npx wrangler secret put KORZEN_API_KEY import { createWorker } from "./bot-protection.js"; export default createWorker();
KORZEN_API_KEY: シークレットキー。Worker の secret として設定します。未設定の場合、すべてのリクエストは判定されずに通過し、Worker のログに一度だけそのことが出ます。KORZEN_URL:https://korzen.dev。KORZEN_TIMEOUT_MS: 判定にかけてよい時間。10〜1000 ms(未設定なら 200)。超えるとリクエストは通過します。KORZEN_REQUIRE_TOKEN: 操作にタグの token が必要なパス。カンマ区切りで、*で終わるものはプレフィックスです。
block の場合、Worker は 403 を返します。block mode で challenge の場合、ページの読み込みはチャレンジページへ移動して token を持って戻り、その token を _kz Cookie として保存します。チャレンジ対象のフォーム送信や API 呼び出しには 403 を返します。それ以外は Cloudflare のキャッシュとオリジンへ進み、アプリケーションでログに残せる Korzen-Request-Id と Korzen-Action ヘッダーが付きます。エラーや timeout の場合はリクエストを通します。Bot Protection が遅いときや停止しているときも、サイトは Worker がない場合と同じように動きます。
token が必要な操作
KORZEN_REQUIRE_TOKEN のパスへの操作(POST、PUT、PATCH、DELETE。ページの読み込みは token を消費しません)は、そのブラウザーでタグが獲得した token がある場合にだけ、セッションごとに 1 回通過します。JavaScript を一度も実行しないスクリプトは、どのアドレスから来ても毎回拒否されます。操作につながるページにタグを置き、そのページも Worker のルートに含めてください。そうすれば、人は読み込み時にチャレンジを受けます。
キャッシュから返されるポーリング
Worker は Cloudflare のキャッシュより前に動きます。キャッシュが返すリクエストはサーバーに届きませんが、Worker には届きます。ポーリングされるルート(在庫や座席表など)に Worker を置けば、すべてのポーリングが判定され、許可されたものは引き続きキャッシュから返されます。Worker のルート上のリクエストはそれぞれプランの判定数に数えられるため、判定したいものだけをルートに含めてください。
Cloudflare が加える情報
Worker は判定ごとに、Cloudflare が訪問者について測定した内容(request.cf)を送ります。全プランでネットワーク(ASN)、国、HTTP バージョン、訪問者と Cloudflare の間の TCP ラウンドトリップ(HTTP/3 ではなし)。Enterprise Bot Management では JA4 フィンガープリントと detection ID も送ります。Bot Protection はこのラウンドトリップを、タグがページで測定したものと照らし合わせます。別のネットワークで記録されたデータは、訪問者自身の接続ではありえないラウンドトリップを主張します。どれも単独で訪問者をブロックすることはありません。
独自のハンドラー
通常は createWorker で足ります。さらに処理を加えたい Worker では、独自の fetch を withKorzen で包んでください。こちらも Cloudflare の測定内容を同じように送ります。
withKorzen は Worker の fetch を包みます。Cloudflare が把握している訪問者の情報を読み取り、お客様の timeout で問い合わせます。block の場合は自身で 403 を返します。block mode で challenge の場合、ページの読み込みはチャレンジページへ移動して token を持って戻り、その token を _kz Cookie として保存します。タグがそのセッションを引き継ぎ、以降の判定は allow になります。それ以外はオリジンに転送され、判定の ID とアクションがヘッダーに入るので、アプリケーションでログに残せます。
// src/index.js: a Worker on the routes you protect, in front of your origin. // wrangler.toml: routes = [{ pattern = "shop.example/signup*", zone_name = "shop.example" }] // The key: npx wrangler secret put KORZEN_API_KEY import { createClient, ipFrom, withKorzen } from "./bot-protection.js"; let protect; // made once per isolate, with the key from env export default { fetch(request, env, ctx) { protect ??= withKorzen( async (request, env, ctx, d) => { // on to your origin, with the decision's id and action const forward = new Request(request); if (d?.request_id) forward.headers.set("Korzen-Request-Id", d.request_id); if (d?.action) forward.headers.set("Korzen-Action", d.action); return fetch(forward); }, { client: createClient({ apiKey: env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }), ip: ipFrom.cloudflare, // CF-Connecting-IP: Cloudflare sets it }, ); return protect(request, env, ctx); }, };
チャレンジされたフォーム送信はページへ移動して戻ることができないため、withKorzen は 403 で拒否します。オリジンで独自の確認を行う場合は onChallenge: () => undefined を渡してください。送信は Korzen-Action: challenge を付けてオリジンに届きます。フォームを表示するページも Worker のルートに含めれば、ページの読み込み時に訪問者がチャレンジされます。
訪問者の IP
CF-Connecting-IP は Cloudflare が設定するもので、訪問者が書き込むことはできないため、Worker は常に正しいアドレスを得られます。ただし、オリジンは Cloudflare からのトラフィックだけを受け付けるようにしてください。そうしないと、bot がオリジンを直接呼び出して Worker を迂回できてしまいます。
ヘッダーとその順序
withKorzen は、ブラウザのビルドごとに値が決まるヘッダー(Accept, Accept-Encoding, client hints, Sec-Fetch-*)と Accept-Language を送ります。ブラウザの User-Agent をまねるスクリプトが、そのすべてをまねることはまれです。ヘッダーの順序(header_order)は送りません。Worker の request.headers は名前を届いた順ではなくソートして並べるため、このフィールドは省かれ、判定は残りを読みます。ご自身の Node サーバーでは、fromNodeRequest が rawHeaders から順序を送ります。
テストする
npx wrangler dev で Worker を手元のマシンで実行します。キーは .dev.vars に KORZEN_API_KEY=… として設定します。その後 npx wrangler deploy を実行し、dashboard の判定ページで判定を確認してください。
よくある間違い
- すべてのルートに Worker を置く。ルート上のリクエストはすべて判定になります。ルートは判定したいもの(監視したいポーリング、新規登録、ログイン、チェックアウト)だけに絞ってください。
- ページにタグがないパスを
KORZEN_REQUIRE_TOKENに入れる。そこでは誰も token を持たないため、すべての操作が拒否されます。 - 判定のためにリクエストのボディを読み、読み終えたボディをそのまま転送する。Request のボディは 1 回しか読めません。
- オリジンがインターネット全体に開いている。Worker が守れるのは Cloudflare を経由するリクエストだけです。