Korzen
Bot Protection

Cloudflare Workers

Bot Protection num Cloudflare Worker, na frente da sua origem: a decisão na borda, o CF-Connecting-IP e a requisição seguindo para a sua aplicação.

Quando o seu site está atrás do Cloudflare, um Worker nas rotas que você protege pode perguntar ao Bot Protection antes de a requisição chegar à sua origem, sem nenhuma mudança na sua aplicação. Ele roda na borda do Cloudflare, perto do korzen.dev, que também está atrás do Cloudflare. Num site estático, uma Cloudflare Pages Function faz o mesmo para uma ação, como no nosso próprio blog.

A tag

A tag continua nas suas páginas, no <head>, para o cookie _kz estar lá quando o visitante enviar o formulário:

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

O Worker

O cliente é um arquivo só, sem dependências: ele pergunta ao /v1/decide, desiste no seu timeout e deixa o visitante passar em qualquer erro. Baixe-o ao lado do seu Worker e leia-o antes do commit:

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

createWorker é o Worker inteiro: ele pergunta sobre cada requisição nas rotas dele com o seu timeout e lê a configuração do ambiente do 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: a sua chave secreta, como secret do Worker. Sem ela, cada requisição segue sem julgamento, e o log do Worker avisa uma vez.
  • KORZEN_URL: https://korzen.dev.
  • KORZEN_TIMEOUT_MS: quanto uma decisão pode levar, de 10 a 1000 ms (200 se não definido). Passado esse tempo, a requisição segue.
  • KORZEN_REQUIRE_TOKEN: os caminhos cujas ações precisam do token da tag, separados por vírgula; um que termina em * é um prefixo.

Em block, o Worker responde 403. Em challenge, no block mode, o carregamento de uma página vai para a página de challenge e volta com um token, que ele guarda como cookie _kz; um envio de formulário ou uma chamada de API com challenge recebe 403. Todo o resto segue para o cache do Cloudflare e a sua origem, com os headers Korzen-Request-Id e Korzen-Action que a sua aplicação pode registrar. Qualquer erro ou timeout deixa a requisição passar: se o Bot Protection estiver lento ou fora do ar, o seu site funciona como sem o Worker.

Ações que precisam de um token

Uma ação num caminho de KORZEN_REQUIRE_TOKEN (o POST, PUT, PATCH ou DELETE dele: o carregamento de uma página não gasta token) só segue com um token que a tag conquistou naquele navegador, uma vez por sessão. Um script que nunca executa JavaScript é recusado em toda tentativa, venha de que endereço vier. Coloque a tag na página que leva à ação, e essa página também nas rotas do Worker, para que uma pessoa encontre o challenge ao carregá-la.

Consultas servidas pelo cache

O Worker roda antes do cache do Cloudflare: uma requisição que o cache responde nunca chega ao seu servidor, mas chega ao Worker. Coloque o Worker nas rotas consultadas em sequência (um estoque, um mapa de assentos) e cada consulta é julgada; as permitidas continuam vindo do cache. Cada requisição nas rotas do Worker é uma decisão no volume do seu plano, então coloque nas rotas só o que você quer julgar.

O que o Cloudflare acrescenta

Com cada decisão, o Worker envia o que o Cloudflare mediu do visitante (request.cf): em todos os planos a rede (ASN), o país, a versão do HTTP e o tempo de ida e volta TCP entre o visitante e o Cloudflare (nenhum sobre HTTP/3); com o Enterprise Bot Management, também o fingerprint JA4 e os detection ids. O Bot Protection compara esse tempo de ida e volta com o que a tag mediu na página: uma gravação feita em outra rede declara um tempo que a própria conexão do visitante não pode ter. Nenhum deles bloqueia um visitante sozinho.

O seu próprio handler

createWorker cobre o caso comum. Para um Worker que faz mais, envolva o seu próprio fetch com withKorzen; ele envia do mesmo jeito o que o Cloudflare mediu.

O withKorzen envolve o fetch do Worker: lê o que o Cloudflare sabe do visitante e pergunta com o seu timeout. Em block, ele mesmo responde 403. Em challenge, no block mode, um carregamento de página vai para a página de desafio e volta com um token, que ele guarda como o cookie _kz: a tag leva essa sessão adiante, e as próximas decisões dela são allow. O resto segue para a sua origem, com o id e a ação da decisão em cabeçalhos que a sua aplicação pode registrar:

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

O envio de um formulário desafiado não pode ir à página e voltar: o withKorzen o recusa com 403. Com uma verificação sua na origem, passe onChallenge: () => undefined, e o envio chega à sua origem com Korzen-Action: challenge. Coloque também a página que mostra o formulário nas rotas do Worker, e o visitante é desafiado quando ela carrega.

O IP do visitante

CF-Connecting-IP é definido pelo Cloudflare e o visitante não consegue escrevê-lo, então um Worker sempre tem o endereço certo. A sua origem ainda deve aceitar tráfego só do Cloudflare, senão um bot pula o Worker chamando-a direto.

Os headers e a ordem deles

withKorzen envia os headers cujos valores uma versão de navegador fixa (Accept, Accept-Encoding, os client hints, Sec-Fetch-*) e Accept-Language: um script que copia o User-Agent de um navegador raramente copia todos eles. Ele não envia a ordem deles (header_order): o request.headers de um Worker lista os nomes em ordem alfabética, não como chegaram, então o campo fica de fora e a decisão lê o resto. No seu próprio servidor Node, fromNodeRequest envia a ordem a partir de rawHeaders.

Teste

npx wrangler dev roda o Worker na sua máquina; defina a chave em .dev.vars como KORZEN_API_KEY=…. Depois npx wrangler deploy, e acompanhe as decisões na página Decisões do painel.

Erros comuns

  • Um Worker em todas as rotas: cada requisição nas rotas dele é uma decisão. Coloque nas rotas só o que você quer julgar: as consultas que você vigia, cadastro, login, checkout.
  • Um caminho em KORZEN_REQUIRE_TOKEN cujas páginas não têm a tag: lá nenhum visitante tem token, então toda ação é recusada.
  • Ler o corpo da requisição para decidir e depois encaminhar um corpo já usado: o corpo de um Request só pode ser lido uma vez.
  • Uma origem aberta para a internet inteira: o Worker só protege as requisições que passam pelo Cloudflare.