Korzen
Bot Protection

Express

Express アプリに Bot Protection を導入します。タグ、Node の fetch による呼び出し、訪問者の IP を得るための trust proxy、アカウントを作成するルートを説明します。

このガイドは、Node 22 以降で動く Express 4 と 5 を対象にしています。Node の fetch はアイドル状態が 4 秒続くと接続を閉じるため、呼び出しの間隔がそれより長いサーバーでは、ほとんどの呼び出しで新しい TLS 接続が開かれます。keepAliveTimeout を長くした undici の Agent を dispatcher として渡すと、接続が維持されます。

タグ

新規登録、ログイン、チェックアウトの各ページを描画するレイアウトの <head> にタグを置きます。

<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

ページでフォームを送信する前に await window.korzen?.ready() を呼び出してください。タグが最後に報告してから訪問者が行ったことを報告し、何が起きても数秒以内に完了します。タグ

呼び出し

クライアントは依存関係のない 1 ファイル、korzen.js です。/v1/decide に問い合わせ、設定した timeout で打ち切り、エラーのときは必ず訪問者を通します。プロジェクトにダウンロードし、commit する前に中身を読んでください。

curl -o korzen.js https://korzen.dev/bot-protection/sdk/korzen.js
curl -o korzen.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript

次に、アカウントを作成するルートに protect を middleware として付けます。応答は req.korzen に入り、応答がなければ null になります。

// app.js
import express from "express";
import { Agent } from "undici"; // optional (npm install undici@7): keeps the connection open between calls
import { createClient, protect } from "./korzen.js";

const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200, dispatcher: new Agent({ keepAliveTimeout: 300_000 }) });
const app = express();
app.set("trust proxy", 1); // one proxy of yours in front: see "The visitor's IP"
app.use(express.urlencoded({ extended: false }));

const guard = protect({
  client: korzen,
  ip: (_get, req) => req.ip, // with "trust proxy" set: see "The visitor's IP"
  onBlock: (req, res) => res.status(400).send("We could not create your account."),
  // onChallenge: (req, res, next, d) => your CAPTCHA, or an email or phone check, then next()
});

app.get("/signup", guard, (req, res) => res.render("signup")); // the form's page: a challenge meets the person here

app.post("/signup", guard, async (req, res) => {
  const user = await createAccount(req.body); // your own code
  await saveRequestID(user.id, req.korzen?.request_id); // see "Keep the request_id"
  res.redirect("/welcome");
});

2026-10-18 に korzen.dev に対して計測しました。Cloudflare は Keep-Alive ヒントを送らないため、Node 18 から 26 では fetch だけだと 5 秒のアイドル後に新しい接続を開き、Agent を使うと 120 秒おきの呼び出しを 1 本の接続でこなしました。undici 7 をインストールしてください。その Agent は Node 20 から 26 の fetch で動きます。バージョンを付けない npm install undici が今入れる undici 8 は、Node 22 と 24 ですべての呼び出しが失敗し(invalid onRequestStart method)、失敗した呼び出しは訪問者を通すので、欠けた判定でしか気づけません。

ハンドラーの中で問い合わせるなら、await korzen.decide(fromNodeRequest(req, { ip: (_get, r) => r.ip, path: "/signup" })) が応答を返し、エラーのときは null を返します。action が allow、challenge、block のいずれでもない応答もエラーとして扱います。理由をログに残すには、createClient に onAnswer を渡してください。各呼び出しの後に、ステータス、Korzen-Request-Id、RateLimit ヘッダー、エラーなら problem の code を受け取ります。

訪問者の IP

前段にいくつプロキシがあるかを Express に伝えない限り、req.ip は接続元のアドレスになります。trust proxy にその数を設定してください(ロードバランサーや nginx が 1 つなら 1)。すると Express は、そのプロキシが X-Forwarded-For に追加したアドレスを使い、それより前に訪問者が書いた値を無視します。true にすると、訪問者が書き込める左端の値を信頼してしまいます。Cloudflare の背後にあり、サーバーが Cloudflare からのトラフィックしか受け付けない場合は、代わりに protect に ip: ipFrom.cloudflare を渡してください。

アクションを適用する

読むのは action だけです。block の場合は、その操作がほかの理由で失敗したときと同じエラーを返し、bot に何も悟らせないようにします。challenge の場合は、すでにある確認手段(CAPTCHA、メールや電話の確認コード)を使います。何もなければそのまま続けます。アカウントは monitor mode で始まり、その間 action は常に allow です。block mode をオンにするまで、訪問者にとっては何も変わりません。または、訪問者を challenge.url のチャレンジページに送ってください。チャレンジページを参照してください。

onChallenge を指定しない場合、middleware は challenge を通しません。ページ読み込み(GET)はチャレンジページへ送られ、フォームの送信や API の呼び出しは 403 で拒否されます。フォームを表示するページも保護してください。人はそこでチャレンジを受け、その通過によってフォームの送信が通ります。独自の確認手段がある場合は、onChallenge を指定してそこで実行してください。戻り先は訪問者が開いたアドレスで、プロキシが付けていれば X-Forwarded-Host と X-Forwarded-Proto から読みます。Host: localhost を転送し、どちらも付けないプロキシ(nginx の既定)の後ろでは publicURL: "https://shop.example" を指定してください。

request_id を保存する

すべての応答には request_id が含まれます。操作で作成されたもの(アカウントや注文)のカラムに保存するか、その ID と一緒にログに残してください。お問い合わせの際にその判定を特定でき、後からどの応答が偽アカウントを通したのかを調べられます。アカウントが良いか悪いかが分かったら、それを送り返してください:await korzen.feedback([{ request_id, outcome: "chargeback" }])(フィードバックを参照)。

手元のマシンでテストする

node --env-file=.env app.js でサーバーを起動し、localhost で開きます。localhost 上のページは常にタグを受け付け、ドメインとして一覧に載ることはありません。ローカルでは IP が 127.0.0.1 または ::1 になりますが、問題ありません。自分のマシンの Docker でアプリを動かすと、代わりにブリッジのアドレス(172.17.0.1 など)になりますが、タグがそこであなたのページを見ているので、チャレンジページは localhost へ戻してくれます。

  1. .env に KORZEN_API_KEY を設定し、新規登録ページを開きます。数秒後、ブラウザーの開発者ツールに _kz Cookie が表示されます。
  2. 新規登録します。判定が dashboard の判定ページに、送信したパスで表示されます。
  3. fail-open を確認します。誤ったキーを設定してもう一度新規登録すると、呼び出しは 401 を返し、新規登録はそのまま完了します。

よくある間違い

  • app.set("trust proxy", true)。すべてのホップを信頼するため、req.ip が訪問者が X-Forwarded-For に書いた値になります。
  • ロードバランサーの背後なのに trust proxy をまったく設定しない。すべての訪問者がロードバランサーのプライベート IP になり、Bot Protection はそれを IP なしとして扱います。見えるものが大きく減り、セットアップページに警告が表示されます。
  • ブラウザーから /v1/decide を呼び出す。API key がすべての訪問者に渡ってしまいます。呼び出しはサーバーで行ってください。
  • タイムアウトがない、またはエラーで新規登録が止まる。200 以外の応答や、時間内に応答がない場合は、訪問者を通すという意味です。
  • ログに korzen: the visitor's IP sent to Bot Protection で始まる行が出る。クライアントは、IP があなた自身のネットワークのもの(ロードバランサー、プロキシ、コンテナのもの)である最初の判定で一度だけこれを書きます。Bot Protection はその判定を IP なしとして扱います。ip に訪問者のアドレスを設定してください(訪問者の IP)。自分のマシンの Docker ではブリッジのアドレスで、そこでは問題ありません。