Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Ephemeral ID による不正検知

最終更新 Markdown で表示Agent セットアップ

Ephemeral ID を使うと、従来の IP ベース検知をすり抜ける不正パターンを見つけられます。このチュートリアルでは、Ephemeral ID を記録し、不審なパターンを検知し、攻撃者をブロックします。

攻撃者は、キャンペーンを悪用するために偽アカウントを何百も作ったり、プロキシプールを回して IP ベースのレート制限を回避したり、実ブラウザーを使って簡易なボット検知をすり抜けたりします。

従来の IP ベース検知は、各リクエストが別のアドレスから来ているように見えるため失敗します。Ephemeral ID は、IP アドレスが変わっても、背後のクライアントデバイスを識別します。

始める前に


ログを用意する

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()
);

Ephemeral ID を取り出して記録する

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 };
}

サインアップフローで Ephemeral ID を使う

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 レピュテーションや行動分析と一緒に使います。

関連リソース

役に立ちましたか?