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:
- Camada 1 — verificação de ID
- Camada 2 — assinatura HMAC
- Camada 3 — lista de IPs
- 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_SECRETcom 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-Forpara 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:
- Confirme que a Camada 1 aceita o
task_idesperado. - Force um
tokeninválido e confirme o 403 da Camada 2. - 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: