Ephemeral ID を使うと、従来の IP ベース検知をすり抜ける不正パターンを見つけられます。このチュートリアルでは、Ephemeral ID を記録し、不審なパターンを検知し、攻撃者をブロックします。
攻撃者は、キャンペーンを悪用するために偽アカウントを何百も作ったり、プロキシプールを回して IP ベースのレート制限を回避したり、実ブラウザーを使って簡易なボット検知をすり抜けたりします。
従来の IP ベース検知は、各リクエストが別のアドレスから来ているように見えるため失敗します。Ephemeral ID は、IP アドレスが変わっても、背後のクライアントデバイスを識別します。
- Ephemeral ID には、Enterprise Turnstile アドオン 付きの Enterprise Bot Management、または スタンドアロンの Enterprise Turnstile が必要です。この機能を有効にするには、アカウントチームに連絡してください。
- Turnstile の基本的な組み込みを知っている必要があります。詳細は Turnstile を始める を参照してください。
Ephemeral ID 付きのイベントを保存するテーブルを作成します。
CREATE TABLE turnstile_events (
id BIGSERIAL PRIMARY KEY,
ephemeral_id VARCHAR(64) NOT NULL,
event_type VARCHAR(50) NOT NULL, -- 'signup', 'login', 'checkout'
ip_address VARCHAR(45),
user_id VARCHAR(128), -- NULL for signups, populated after
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE blocked_ephemeral_ids (
ephemeral_id VARCHAR(64) PRIMARY KEY,
reason VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW()
);Siteverify を呼ぶと、Ephemeral ID は metadata フィールドに返ります。保護対象の操作ごとに記録します。
async function verifyAndLogTurnstile(
token: string,
ip: string,
secretKey: string,
eventType: string,
db: Database,
): Promise<{ success: boolean; ephemeralId?: string; isBlocked: boolean }> {
// Call Siteverify API
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: secretKey,
response: token,
remoteip: ip,
}),
},
);
const result = await response.json();
if (!result.success) {
return { success: false, isBlocked: false };
}
const ephemeralId = result.metadata?.ephemeral_id;
if (ephemeralId) {
// Log the event
await db.query(
`INSERT INTO turnstile_events (ephemeral_id, event_type, ip_address)
VALUES ($1, $2, $3)`,
[ephemeralId, eventType, ip],
);
// Check if already blocked
const blocked = await db.query(
`SELECT 1 FROM blocked_ephemeral_ids WHERE ephemeral_id = $1`,
[ephemeralId],
);
if (blocked.rows.length > 0) {
return { success: true, ephemeralId, isBlocked: true };
}
}
return { success: true, ephemeralId, isBlocked: false };
}export async function handleSignup(request: Request, env: Env) {
const formData = await request.formData();
const email = formData.get("email") as string;
const turnstileToken = formData.get("cf-turnstile-response") as string;
const ip = request.headers.get("CF-Connecting-IP") || "";
// Verify Turnstile and log the Ephemeral ID
const verification = await verifyAndLogTurnstile(
turnstileToken,
ip,
env.TURNSTILE_SECRET_KEY,
"signup",
env.DB,
);
if (!verification.success) {
return new Response("Verification failed", { status: 400 });
}
// Block if this device is flagged
if (verification.isBlocked) {
// Return a generic message - don't reveal detection
return new Response("Please verify your email to continue", {
status: 202,
});
}
// Proceed with normal signup
const userId = await createUser(email, formData.get("password"));
// Update the log with the new user ID
if (verification.ephemeralId) {
await env.DB.query(
`UPDATE turnstile_events
SET user_id = $1
WHERE ephemeral_id = $2 AND event_type = 'signup' AND user_id IS NULL
ORDER BY created_at DESC LIMIT 1`,
[userId, verification.ephemeralId],
);
}
return new Response("Account created", { status: 201 });
}次のクエリを定期的に(例: 5 分ごと)実行し、不審な Ephemeral ID を探します。
-- Find devices creating multiple accounts in the last hour
SELECT
ephemeral_id,
COUNT(*) as signup_count,
COUNT(DISTINCT ip_address) as unique_ips
FROM turnstile_events
WHERE
event_type = 'signup'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY ephemeral_id
HAVING COUNT(*) > 3; -- More than 3 signups = suspicious不審な ID が見つかったら、ブロックします。
INSERT INTO blocked_ephemeral_ids (ephemeral_id, reason)
SELECT
ephemeral_id,
'Multiple signups: ' || COUNT(*) || ' in 1 hour'
FROM turnstile_events
WHERE
event_type = 'signup'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY ephemeral_id
HAVING COUNT(*) > 3
ON CONFLICT (ephemeral_id) DO NOTHING;不正でアカウントを禁止したときは、同じデバイスからのほかのアカウントを探します。
-- Find all accounts created from the same device as a banned user
SELECT DISTINCT te2.user_id, te2.created_at
FROM turnstile_events te1
JOIN turnstile_events te2 ON te1.ephemeral_id = te2.ephemeral_id
WHERE te1.user_id = 'BANNED_USER_ID'
AND te2.user_id IS NOT NULL
AND te2.user_id != 'BANNED_USER_ID';一括でアカウントにレビューフラグを付けます。
-- Flag all accounts from a suspicious device
UPDATE users
SET status = 'under_review'
WHERE id IN (
SELECT DISTINCT user_id
FROM turnstile_events
WHERE ephemeral_id = 'x:SUSPICIOUS_ID_HERE'
AND user_id IS NOT NULL
);- すぐに記録する: Siteverify を呼んだ直後に Ephemeral ID を取得します。
- 静かに拒否する: 不正をブロックするときは、一般的なエラーを返します。デバイスを検知したことは決して明かさないでください。
- しきい値を調整する: クエリは控えめに始めます(例: 1 時間あたり 3 回のサインアップ)。トラフィックに合わせて調整します。
- シグナルを組み合わせる: Ephemeral ID を、IP レピュテーションや行動分析と一緒に使います。