Lorsque vous utilisez la fonctionnalité d'URL de rappel de CaptchaAI (pingback), votre serveur expose un point de terminaison HTTP qui reçoit les solutions CAPTCHA. Sans validation, quiconque découvre cette URL peut envoyer de fausses solutions. Ce didacticiel explique comment sécuriser les points de terminaison de rappel.
Le flux de rappel
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
Le problème : l’étape 3 est une demande non authentifiée. Vous devez vérifier qu'il provient bien de CaptchaAI.
Stratégie de validation 1 : vérification de l'ID de la tâche
L'approche la plus simple : n'acceptez que les résultats de rappel pour les ID de tâche que vous avez réellement soumis.
Python (flacon)
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);
Stratégie de validation 2 : jeton de signature HMAC
Ajoutez un jeton secret à votre URL de rappel que les attaquants ne peuvent pas deviner.
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);
});
Utilisez l'URL générée lors de la soumission : pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.
Stratégie de validation 3 : liste autorisée d'adresses IP
Limitez votre point de terminaison de rappel aux adresses IP du serveur de CaptchaAI.
Python (flacon)
# 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();
});
Remarque : Contactez l'assistance CaptchaAI pour connaître la liste actuelle des adresses IP sources de rappel. Si vous êtes derrière un proxy inverse, assurez-vous que les en-têtes
X-Forwarded-Forsont correctement configurés.
Prévention des attaques par relecture
Même les rappels valides peuvent être rejoués. Ajoutez une vérification d'horodatage et une application à usage unique :
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
Liste de contrôle de sécurité combinée
| Calque | Protection contre | Mise en œuvre |
|---|---|---|
| Vérification de l'ID de tâche | Injection de tâches Random/unknown | Stockez les identifiants en attente, rejetez les inconnus |
| Signature HMAC | Devinettes d'URL, rappels falsifiés | Signer l'URL de rappel avec un secret |
| Liste blanche d'adresses IP | Requêtes provenant de serveurs non autorisés | Liste blanche des adresses IP CaptchaAI |
| Prévention des rediffusions | Rappels valides soumis à nouveau | Usage unique + validation d'horodatage |
| HTTPS | Écoute clandestine, homme du milieu | TLS sur le point de terminaison de rappel |
Dépannage
| Problème | Parce que | Corriger |
|---|---|---|
| Tous les rappels rejetés | La liste d'adresses IP autorisées n'inclut pas les adresses IP CaptchaAI | Vérifiez les adresses IP actuelles avec le support ; vérifier les en-têtes de proxy inverse |
| La vérification HMAC échoue | Incompatibilité d'ID de tâche entre la soumission et le rappel | Assurez-vous d'utiliser l'ID de tâche exact renvoyé par in.php |
| Rappels en double traités | Condition de concurrence sur les rappels simultanés | Utiliser des opérations d'ensemble atomique ou des contraintes uniques de base de données |
| Les rappels expirent | Le point de terminaison met trop de temps à répondre | Traiter de manière asynchrone : accepter immédiatement, traiter en arrière-plan |
FAQ
Dois-je utiliser les quatre stratégies de validation ensemble ?
Utilisez la vérification de l’ID de tâche (Stratégie 1) au minimum. Ajoutez des signatures HMAC (Stratégie 2) pour les points de terminaison publics. La liste blanche d'adresses IP (stratégie 3) est idéale si CaptchaAI publie des adresses IP de rappel stables. La prévention des rediffusions est essentielle pour les flux de travail financiers ou sensibles.
Que se passe-t-il si mon point de terminaison de rappel est en panne lorsque CaptchaAI envoie le résultat ?
La solution est toujours disponible via le point de terminaison d'interrogation (res.php). Implémentez une solution de secours qui interroge toutes les tâches qui ne reçoivent pas de rappels dans un délai d'expiration.
Puis-je utiliser le protocole TLS mutuel (mTLS) pour l'authentification par rappel ?
En théorie, oui, mais le système de rappel de CaptchaAI utilise des requêtes HTTPS GET standard. Les signatures HMAC fournissent une authentification équivalente sans nécessiter de gestion de certificat.
Articles connexes
- Modèles de gestion des erreurs de rappel Captchaai
- Guide du webhook de l'URL de rappel Captchaai
Prochaines étapes
Sécurisez vos points de terminaison de rappel CaptchaAI -récupérez votre clé APIet mettre en œuvre la validation des signatures.
Guides associés :