الدروس التطبيقية

CaptchaAI Webhook Security: التحقق من صحة تواقيع رد الاتصال

عند استخدام ميزة عنوان URL لرد الاتصال الخاصة بـ CaptchaAI (pingback)، يكشف الخادم الخاص بك عن نقطة نهاية HTTP التي تتلقى حلول CAPTCHA. بدون التحقق من الصحة، يمكن لأي شخص يكتشف عنوان URL هذا إرسال حلول مزيفة. يغطي هذا البرنامج التعليمي كيفية تأمين نقاط نهاية رد الاتصال.

تدفق رد الاتصال


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

المشكلة: الخطوة 3 هي طلب لم تتم مصادقته. تحتاج إلى التحقق من أنه جاء بالفعل من CaptchaAI.

استراتيجية التحقق 1: التحقق من معرف المهمة

الطريقة الأبسط هي قبول نتائج رد الاتصال لمعرفات المهام التي أرسلتها بالفعل فقط.

بايثون (قارورة)

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 (اكسبرس)

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

استراتيجية التحقق 2: رمز توقيع HMAC

أضف رمزًا سريًا إلى عنوان URL لرد الاتصال الخاص بك والذي لا يستطيع المهاجمون تخمينه.

بايثون

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

استخدم عنوان URL الذي تم إنشاؤه عند الإرسال: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.

استراتيجية التحقق 3: القائمة المسموح بها لعنوان IP

قم بتقييد نقطة نهاية رد الاتصال الخاصة بك على عناوين IP لخادم CaptchaAI.

بايثون (قارورة)

# 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 (اكسبرس)

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

ملاحظة: اتصل بدعم CaptchaAI للحصول على القائمة الحالية لعناوين IP لمصدر رد الاتصال. إذا كنت تستخدم وكيلًا عكسيًا، فتأكد من تكوين رؤوس X-Forwarded-For بشكل صحيح.

إعادة منع الهجوم

حتى عمليات الاسترجاعات الصالحة يمكن إعادة تشغيلها. أضف فحص الطابع الزمني وفرض الاستخدام لمرة واحدة:

بايثون

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

قائمة التحقق الأمنية المجمعة

طبقة الحماية ضد التنفيذ
التحقق من معرف المهمة Random/unknown حقن المهام تخزين المعرفات المعلقة، ورفض المجهولين
توقيع HMAC تخمين عنوان URL، وعمليات الاسترجاعات المزورة قم بتسجيل عنوان URL لرد الاتصال بالسر
القائمة المسموح بها لعناوين IP الطلبات من خوادم غير مصرح بها القائمة البيضاء لعناوين IP CaptchaAI
منع إعادة التشغيل تمت إعادة إرسال عمليات الاسترجاعات الصالحة الاستخدام لمرة واحدة + التحقق من صحة الطابع الزمني
HTTPS التنصت، أيها الرجل في المنتصف TLS على نقطة نهاية رد الاتصال

استكشاف الأخطاء وإصلاحها

المشكلة السبب الإجراء
يُنشأ الرمز المميز لكن الجهة المستهدفة ترفضه مفتاح الموقع أو الصفحة أو سياق الجلسة لا يتطابق التقط المعلمات من جديد وأعد استخدام الرمز داخل جلسة HTTP أو المتصفح نفسها
تنتهي عملية الاستطلاع بمهلة الفاصل الزمني أو وقت الانتظار أو معالجة الأخطاء صارمة أكثر من اللازم استطلع كل 5 إلى 10 ثوانٍ وافصل بين انتهاء المهلة والأخطاء الفعلية وسجّل السبب
ينجح المثال محليًا لكنه يفشل داخل سير العمل رد النداء أو حقل النموذج أو حقن الرمز مفقود في السلسلة الفعلية تحقق من المسار الكامل بين أداة الحل والطلب النهائي إلى الموقع المستهدف

الخطوات التالية

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.