Tutorials

Segurança de webhook CaptchaAI: como validar assinaturas de callback

Todo endpoint que recebe callbacks HTTP tem o mesmo ponto fraco: quem descobrir a URL pode enviar dados falsos para ele.

Com o pingback da CaptchaAI, isso significa aceitar "soluções" falsas como legítimas. Este guia traz quatro camadas, da mais simples à mais rigorosa.

Ordem recomendada:

  1. Camada 1 — verificação de ID
  2. Camada 2 — assinatura HMAC
  3. Camada 3 — lista de IPs
  4. Camada 4 — bloqueio de replay

Como funciona o callback da CaptchaAI


1. You submit task:
   POST https://ocr.captchaai.com/in.php
     ?key=YOUR_API_KEY
     &method=userrecaptcha
     &googlekey=SITE_KEY
     &pageurl=https://example.com
     &pingback=https://your-server.com/captcha/callback

2. CaptchaAI solves the CAPTCHA

3. CaptchaAI sends result to your endpoint:
   GET https://your-server.com/captcha/callback?id=TASK_ID&code=SOLUTION_TOKEN

O ponto cego está no passo 3: essa requisição GET chega sem autenticação embutida.

Nada garante que partiu da CaptchaAI — só o formato da URL, que qualquer um reproduz.


Camada 1: aceite apenas callbacks de tarefas que você enviou

É a defesa mínima: aceite só resultados cujo ID você registrou ao enviar a in.php.

ID desconhecido é rejeitado antes da lógica de negócio.

Dica: registre os callbacks rejeitados (IP, ID, horário) — primeiro lugar a checar numa tentativa de fraude.

Python (Flask)

import os
import threading
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# Thread-safe set of pending task IDs
pending_tasks = set()
pending_lock = threading.Lock()
results = {}

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


def submit_captcha(sitekey, pageurl):
    """Submit CAPTCHA and register the task ID."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": "https://your-server.com/captcha/callback",
        "json": 1
    })
    data = resp.json()

    if data.get("status") == 1:
        task_id = data["request"]
        with pending_lock:
            pending_tasks.add(task_id)
        return task_id
    return None


@app.route("/captcha/callback")
def captcha_callback():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    # Validate: only accept known task IDs
    with pending_lock:
        if task_id not in pending_tasks:
            return jsonify({"error": "unknown task"}), 403
        pending_tasks.discard(task_id)

    results[task_id] = solution
    return "OK", 200

JavaScript (Express)

const express = require("express");
const axios = require("axios");

const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const pendingTasks = new Set();
const results = new Map();

async function submitCaptcha(sitekey, pageurl) {
  const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: sitekey,
      pageurl: pageurl,
      pingback: "https://your-server.com/captcha/callback",
      json: 1,
    },
  });

  if (resp.data.status === 1) {
    const taskId = resp.data.request;
    pendingTasks.add(taskId);
    return taskId;
  }
  return null;
}

app.get("/captcha/callback", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  // Validate: only accept known task IDs
  if (!pendingTasks.has(taskId)) {
    return res.status(403).json({ error: "unknown task" });
  }

  pendingTasks.delete(taskId);
  results.set(taskId, solution);
  res.sendStatus(200);
});

app.listen(3000);

Camada 2: assine a URL de callback com HMAC

A verificação de ID barra tarefas desconhecidas, mas não impede alguém de adivinhar um ID e forjar um resultado.

A Camada 2 fecha a brecha: assine a URL com HMAC, usando um segredo que nunca sai do servidor.

Dica: CALLBACK_SECRET com 32+ caracteres; rotacione se vazar em logs.

Python

import hashlib
import hmac
import os

CALLBACK_SECRET = os.environ["CALLBACK_SECRET"]  # Random 32+ character string


def generate_callback_url(task_id):
    """Generate callback URL with HMAC signature."""
    signature = hmac.new(
        CALLBACK_SECRET.encode(),
        task_id.encode(),
        hashlib.sha256
    ).hexdigest()

    return f"https://your-server.com/captcha/callback?token={signature}"


@app.route("/captcha/callback")
def captcha_callback():
    task_id = request.args.get("id")
    token = request.args.get("token")
    solution = request.args.get("code")

    # Verify HMAC signature
    expected = hmac.new(
        CALLBACK_SECRET.encode(),
        task_id.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(token, expected):
        return jsonify({"error": "invalid signature"}), 403

    results[task_id] = solution
    return "OK", 200

JavaScript

const crypto = require("crypto");

const CALLBACK_SECRET = process.env.CALLBACK_SECRET;

function generateCallbackUrl(taskId) {
  const signature = crypto
    .createHmac("sha256", CALLBACK_SECRET)
    .update(taskId)
    .digest("hex");

  return `https://your-server.com/captcha/callback?token=${signature}`;
}

app.get("/captcha/callback", (req, res) => {
  const taskId = req.query.id;
  const token = req.query.token;
  const solution = req.query.code;

  // Verify HMAC signature
  const expected = crypto
    .createHmac("sha256", CALLBACK_SECRET)
    .update(taskId)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected))) {
    return res.status(403).json({ error: "invalid signature" });
  }

  results.set(taskId, solution);
  res.sendStatus(200);
});

Envie essa URL gerada no parâmetro pingback: https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.


Camada 3: restrinja o callback aos IPs da CaptchaAI

Prefere não manter lógica de assinatura? Restrinja o endpoint aos IPs que a CaptchaAI usa para enviar callbacks.

É a camada mais simples, mas depende de uma lista de IPs estável.

Python (Flask)

# CaptchaAI callback source IPs (verify current IPs with CaptchaAI support)
ALLOWED_IPS = {"138.201.XX.XX", "148.251.XX.XX"}  # Replace with actual IPs


@app.before_request
def check_ip():
    if request.path.startswith("/captcha/callback"):
        client_ip = request.remote_addr
        if client_ip not in ALLOWED_IPS:
            return jsonify({"error": "forbidden"}), 403

JavaScript (Express)

const ALLOWED_IPS = new Set(["138.201.XX.XX", "148.251.XX.XX"]);

app.use("/captcha/callback", (req, res, next) => {
  const clientIp = req.ip || req.connection.remoteAddress;
  if (!ALLOWED_IPS.has(clientIp)) {
    return res.status(403).json({ error: "forbidden" });
  }
  next();
});

Observação: confirme a lista atual de IPs com o suporte antes de ativar essa camada. Atrás de proxy reverso, valide o X-Forwarded-For para não capturar o IP errado.


Camada 4: bloqueie replay de callbacks válidos

Nenhuma das três camadas anteriores impede que um callback legítimo seja capturado e reenviado depois.

Combine timestamp e uma marca de "já processado" por ID de tarefa para fechar essa lacuna.

Python

import time

CALLBACK_TTL = 300  # Reject callbacks older than 5 minutes
used_callbacks = set()


@app.route("/captcha/callback")
def captcha_callback():
    task_id = request.args.get("id")
    timestamp = request.args.get("ts")
    solution = request.args.get("code")

    # Check timestamp freshness
    if timestamp:
        age = time.time() - float(timestamp)
        if age > CALLBACK_TTL or age < 0:
            return jsonify({"error": "expired"}), 403

    # One-time use
    if task_id in used_callbacks:
        return jsonify({"error": "already processed"}), 409

    used_callbacks.add(task_id)
    results[task_id] = solution
    return "OK", 200

Dica: sincronize o relógio via NTP — poucos segundos de desvio já derrubam um callback válido.

No backend em sa-east-1 ou com dados de usuários brasileiros, registre cada callback rejeitado.

É evidência de controle de acesso para a LGPD.

Quando usar cada camada:

  • Sempre: Camada 1
  • Público: + Camada 2
  • IP estável: + Camada 3
  • Sensível a duplicidade: + Camada 4

Checklist combinado de segurança

Camada Protege contra Como implementar
Verificação de ID ID desconhecido Armazenar IDs pendentes
Assinatura HMAC URL adivinhada Assinar com um segredo
Lista de permissões de IP Servidor não autorizado Restringir aos IPs da CaptchaAI
Prevenção de replay Callback reenviado Uso único + timestamp
HTTPS Man-in-the-middle TLS no endpoint

Problemas comuns na validação de callback

Sintoma Causa provável Correção
Todos os callbacks rejeitados Lista de IPs desatualizada Confirme com o suporte; revise o proxy reverso
HMAC falha sempre ID da assinatura difere do callback Use o task_id exato retornado por in.php
Callbacks duplicados processados Condição de corrida Operação atômica ou restrição UNIQUE no banco
Callbacks expiram antes de processar Endpoint demora a responder Responda assíncrono; processe depois

Perguntas frequentes

Por onde começar se o tempo for curto?

Implemente a verificação de ID (Camada 1) primeiro — é a mais barata e já barra a maioria dos callbacks forjados. Adicione HMAC assim que o endpoint ficar público; as outras duas podem esperar.

O que acontece se meu servidor estiver fora do ar quando a CaptchaAI enviar o callback?

A solução continua disponível via res.php. Faça polling das tarefas sem callback confirmado.

Como testo a validação de callback sem expor o endpoint de produção?

Rode o servidor em https://staging.example.com/captcha/callback e siga esta sequência:

  1. Confirme que a Camada 1 aceita o task_id esperado.
  2. Force um token inválido e confirme o 403 da Camada 2.
  3. Reenvie o mesmo callback e confirme o 409 da Camada 4.

A assinatura HMAC já cobre a prevenção de replay?

Não sozinha. O HMAC garante a origem do callback, mas não impede reenvio da mesma URL válida. Combine com a Camada 4 em fluxos sensíveis a duplicidade.

Preciso de HTTPS mesmo já usando lista de permissões de IP?

Sim. A lista de IPs restringe quem chama o endpoint, mas não criptografa o tráfego — sem TLS, o token trafega em texto puro.


Artigos relacionados


Próximas etapas

Proteja seu endpoint de callback agora: gere sua chave de API.

Aplique pelo menos a verificação de ID de tarefa antes de aceitar o primeiro resultado em produção.

Guias relacionados:

Os comentários estão desativados para este artigo.