Tutoriels

Sécurité du webhook CaptchaAI : validation des signatures de rappel

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-For sont 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

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 :

Les commentaires sont désactivés pour cet article.