Tutoriais

Segurança do webhook CaptchaAI: validando assinaturas de retorno de chamada

Quando você usa o recurso de URL de retorno de chamada do CaptchaAI (pingback), seu servidor expõe um terminal HTTP que recebe soluções CAPTCHA. Sem validação, quem descobrir aquela URL pode enviar soluções falsas. Este tutorial aborda como proteger endpoints de retorno de chamada.

O fluxo de retorno de chamada


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 problema: a etapa 3 é uma solicitação não autenticada. Você precisa verificar se realmente veio de CaptchaAI.

Estratégia de validação 1: verificação de ID de tarefa

A abordagem mais simples: aceite apenas resultados de retorno de chamada para IDs de tarefas que você realmente enviou.

Python (frasco)

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

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

Estratégia de validação 2: token de assinatura HMAC

Adicione um token secreto ao seu URL de retorno de chamada que os invasores não consigam adivinhar.

Pitão

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

Use a URL gerada ao enviar: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.

Estratégia de validação 3: lista de permissões de IP

Restrinja seu endpoint de retorno de chamada aos IPs do servidor CaptchaAI.

Python (frasco)

# 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 (Expresso)

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: Entre em contato com o suporte CaptchaAI para obter a lista atual de IPs de origem de retorno de chamada. Se você estiver atrás de um proxy reverso, certifique-se de que os cabeçalhos X-Forwarded-For estejam configurados corretamente.

Prevenção de ataques de repetição

Até mesmo retornos de chamada válidos podem ser reproduzidos. Adicione uma verificação de carimbo de data/hora e aplicação de uso único:

Pitão

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

Lista de verificação de segurança combinada

Camada Proteção contra Implementação
Verificação de ID de tarefa Injeção de tarefa Random/unknown Armazene IDs pendentes, rejeite desconhecidos
Assinatura HMAC Adivinhação de URL, retornos de chamada forjados Assinar URL de retorno de chamada com segredo
Lista de permissões de IP Solicitações de servidores não autorizados Lista de permissões de IPs CaptchaAI
Prevenção de repetição Retornos de chamada válidos reenviados Validação de uso único + carimbo de data/hora
HTTPS Espionagem, homem do meio TLS no terminal de retorno de chamada

Solução de problemas

Problema Causa Correção
Todos os retornos de chamada foram rejeitados A lista de permissões de IP não inclui IPs CaptchaAI Verifique IPs atuais com suporte; verifique os cabeçalhos do proxy reverso
A verificação HMAC falha Incompatibilidade de ID de tarefa entre envio e retorno de chamada Certifique-se de usar o ID de tarefa exato retornado por in.php
Retornos de chamada duplicados processados Condição de corrida em retornos de chamada simultâneos Use operações de conjunto atômico ou restrições exclusivas do banco de dados
Tempo limite dos retornos de chamada O endpoint demora muito para responder Processar assíncrono – aceitar imediatamente, processar em segundo plano

Perguntas frequentes

Devo usar todas as quatro estratégias de validação juntas?

Use a verificação de ID de tarefa (Estratégia 1) como mínimo. Adicione assinaturas HMAC (Estratégia 2) para endpoints voltados ao público. A lista de permissões de IP (Estratégia 3) é ideal se CaptchaAI publicar IPs de retorno de chamada estáveis. A prevenção de repetição é essencial para fluxos de trabalho financeiros ou confidenciais.

O que acontece se meu endpoint de retorno de chamada estiver inativo quando CaptchaAI enviar o resultado?

A solução ainda está disponível através do endpoint de pesquisa (res.php). Implemente um substituto que pesquise quaisquer tarefas que não recebam retornos de chamada dentro de um período de tempo limite.

Posso usar TLS mútuo (mTLS) para autenticação de retorno de chamada?

Em teoria, sim, mas o sistema de retorno de chamada do CaptchaAI usa solicitações HTTPS GET padrão. As assinaturas HMAC fornecem autenticação equivalente sem exigir gerenciamento de certificado.

Artigos relacionados

Próximas etapas

Proteja seus endpoints de retorno de chamada CaptchaAI -obtenha sua chave APIe implementar validação de assinatura.

Guias relacionados:

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