Korzen
Bot Protection

Cloudflare Workers

Bot Protection in un Cloudflare Worker, davanti alla tua origin: la decisione all'edge, CF-Connecting-IP, e la richiesta che prosegue verso la tua app.

Quando il tuo sito è dietro Cloudflare, un Worker sulle route che proteggi può interrogare Bot Protection prima che la richiesta raggiunga la tua origin: senza alcuna modifica alla tua applicazione. Gira all'edge di Cloudflare, vicino a korzen.dev, che è anch'esso dietro Cloudflare. Su un sito statico, una Cloudflare Pages Function fa lo stesso per una singola azione, come sul nostro blog.

Il tag

Il tag va comunque sulle tue pagine, nel <head>, così il cookie _kz c'è quando il visitatore invia il form:

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

Il Worker

Il client è un solo file senza dipendenze: chiede a /v1/decide, rinuncia allo scadere del tuo timeout e lascia passare il visitatore a ogni errore. Scaricalo accanto al tuo Worker e leggilo prima del commit:

curl -o src/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js

createWorker è l'intero Worker: chiede di ogni richiesta sulle sue route con il tuo timeout, e prende le impostazioni dall'ambiente del 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: la tua chiave segreta, come secret del Worker. Senza, ogni richiesta prosegue senza giudizio, e il log del Worker lo dice una volta.
  • KORZEN_URL: https://korzen.dev.
  • KORZEN_TIMEOUT_MS: quanto può durare una decisione, da 10 a 1000 ms (200 se non impostato). Oltre, la richiesta prosegue.
  • KORZEN_REQUIRE_TOKEN: i path le cui azioni richiedono il token del tag, separati da virgole; uno che finisce con * è un prefisso.

Con block il Worker risponde 403. Con challenge, in block mode, un caricamento di pagina va alla pagina di challenge e torna con un token, che salva come cookie _kz; un invio di form o una chiamata API sotto challenge riceve 403. Tutto il resto prosegue verso la cache di Cloudflare e la tua origin, con gli header Korzen-Request-Id e Korzen-Action che la tua applicazione può registrare. Qualsiasi errore o timeout lascia passare la richiesta: se Bot Protection è lento o giù, il tuo sito funziona come senza il Worker.

Azioni che richiedono un token

Un'azione su un path in KORZEN_REQUIRE_TOKEN (il suo POST, PUT, PATCH o DELETE: un caricamento di pagina non spende token) prosegue solo con un token che il tag ha guadagnato in quel browser, una volta per sessione. Uno script che non esegue mai JavaScript viene rifiutato a ogni tentativo, da qualunque indirizzo arrivi. Metti il tag sulla pagina che porta all'azione, e quella pagina anche sulle route del Worker, così una persona incontra il challenge quando la carica.

Polling servito dalla cache

Il Worker gira prima della cache di Cloudflare: una richiesta a cui risponde la cache non arriva mai al tuo server, ma arriva al Worker. Metti il Worker sulle route interrogate di continuo (un inventario, una mappa dei posti) e ogni richiesta viene giudicata; quelle permesse sono ancora servite dalla cache. Ogni richiesta sulle route del Worker è una decisione nel volume del tuo piano, quindi instrada solo ciò che vuoi giudicare.

Cosa aggiunge Cloudflare

Con ogni decisione il Worker invia ciò che Cloudflare ha misurato del visitatore (request.cf): in ogni piano la rete (ASN), il paese, la versione HTTP e il round trip TCP tra il visitatore e Cloudflare (nessuno su HTTP/3); con Enterprise Bot Management anche il fingerprint JA4 e i detection id. Bot Protection confronta quel round trip con quello che il tag ha misurato sulla pagina: una registrazione fatta su un'altra rete dichiara un round trip che la connessione del visitatore non può avere. Nessuno di questi blocca un visitatore da solo.

Un handler tuo

createWorker copre il caso comune. Per un Worker che fa di più, avvolgi il tuo fetch con withKorzen; invia allo stesso modo ciò che Cloudflare ha misurato.

withKorzen avvolge il fetch del Worker: legge quello che Cloudflare sa del visitatore e chiede con il tuo timeout. Su block risponde da sé 403. Su challenge, in block mode, il caricamento di una pagina va alla pagina di challenge e torna con un token, che tiene come cookie _kz: il tag porta avanti quella sessione, e le sue decisioni successive sono allow. Tutto il resto prosegue verso la tua origine, con l'id e l'azione della decisione in header che la tua applicazione può registrare:

// 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);
  },
};

L'invio di un form in challenge non può andare alla pagina e tornare: withKorzen lo rifiuta con 403. Con una tua verifica sull'origine, passa onChallenge: () => undefined, e l'invio arriva alla tua origine con Korzen-Action: challenge. Metti anche la pagina che mostra il form sulle route del Worker, e il visitatore riceve la challenge quando si carica.

L'IP del visitatore

CF-Connecting-IP lo imposta Cloudflare e il visitatore non può scriverlo, quindi un Worker ha sempre l'indirizzo giusto. La tua origin dovrebbe comunque accettare traffico solo da Cloudflare, altrimenti un bot può saltare il Worker chiamandola direttamente.

Gli header e il loro ordine

withKorzen invia gli header i cui valori una versione di browser fissa (Accept, Accept-Encoding, i client hints, Sec-Fetch-*) e Accept-Language: uno script che copia lo User-Agent di un browser raramente li copia tutti. Non ne invia l'ordine (header_order): in un Worker request.headers elenca i nomi in ordine alfabetico, non come sono arrivati, quindi il campo resta fuori e la decisione legge il resto. Sul tuo server Node, fromNodeRequest invia l'ordine da rawHeaders.

Provalo

npx wrangler dev esegue il Worker sulla tua macchina; imposta la chiave in .dev.vars come KORZEN_API_KEY=…. Poi npx wrangler deploy, e osserva le decisioni nella pagina Decisioni della dashboard.

Errori comuni

  • Un Worker su ogni route: ogni richiesta sulle sue route è una decisione. Instrada solo ciò che vuoi giudicare: il polling che tieni d'occhio, registrazione, login, checkout.
  • Un path in KORZEN_REQUIRE_TOKEN le cui pagine non hanno il tag: lì nessun visitatore ha un token, quindi ogni azione viene rifiutata.
  • Leggere il body della richiesta per decidere, e poi inoltrare un body già consumato: il body di una Request si può leggere una volta sola.
  • Una origin aperta a tutta internet: il Worker protegge solo le richieste che passano da Cloudflare.