Tutorial

Keamanan Webhook CaptchaAI: Memvalidasi Tanda Tangan Callback

Saat Anda menggunakan fitur URL callback CaptchaAI (pingback), server Anda mengekspos endpoint HTTP yang menerima solusi CAPTCHA. Tanpa validasi, siapa pun yang menemukan URL tersebut dapat mengirimkan solusi palsu. Tutorial ini membahas cara mengamankan endpoint callback.

Alur Callback


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

Masalahnya: langkah 3 adalah request yang tidak terautentikasi. Anda perlu memverifikasi bahwa itu benar-benar berasal dari CaptchaAI.

Strategi Validasi 1: Verifikasi Task ID

Pendekatan paling sederhana — hanya terima hasil callback untuk task ID yang benar-benar Anda submit.

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

Strategi Validasi 2: HMAC Signature Token

Tambahkan token rahasia ke URL callback Anda yang tidak dapat ditebak penyerang.

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

Gunakan URL yang dihasilkan saat submit: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.

Strategi Validasi 3: IP Allowlist

Batasi endpoint callback Anda ke IP server CaptchaAI.

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

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

Catatan: Hubungi dukungan CaptchaAI untuk mengetahui daftar IP sumber callback terkini. Jika Anda menggunakan reverse proxy, pastikan header X-Forwarded-For dikonfigurasi dengan benar.

Pencegahan Replay Attack

Bahkan callback yang valid bisa di-replay. Tambahkan pemeriksaan timestamp dan one-time enforcement:

Python = 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

```

Checklist Keamanan Gabungan

Lapisan Perlindungan Terhadap Implementasi
Verifikasi Task ID Task injeksi acak/tidak dikenal Simpan ID pending, tolak yang tidak dikenal
HMAC signature Menebak URL, callback palsu Tanda tangani URL callback dengan secret
IP allowlist Request dari server tidak sah Whitelist IP CaptchaAI
Pencegahan replay Mengirim ulang callback valid Validasi timestamp + one-time use
HTTPS Penyadapan, man-in-the-middle TLS pada endpoint callback

Pemecahan Masalah

Masalah Penyebab Solusi
Semua callback ditolak IP allowlist tidak mencakup IP CaptchaAI Verifikasi IP terkini dengan support; periksa header reverse proxy
Verifikasi HMAC gagal Task ID tidak cocok antara submit dan callback Pastikan menggunakan task ID persis yang dikembalikan oleh in.php
Callback duplikat diproses Race condition pada callback bersamaan Gunakan operasi set atomik atau unique constraint database
Callback timeout Endpoint butuh terlalu lama merespons Proses async — segera terima, proses di background

Pertanyaan Umum

Haruskah saya menggunakan keempat strategi validasi sekaligus?

Gunakan verifikasi task ID (Strategi 1) sebagai minimum. Tambahkan HMAC signature (Strategi 2) untuk endpoint yang dapat diakses publik. IP allowlist (Strategi 3) ideal jika CaptchaAI menerbitkan IP callback yang stabil. Pencegahan replay sangat penting untuk workflow finansial atau sensitif.

Apa yang terjadi jika endpoint callback saya mati saat CaptchaAI mengirim hasilnya?

Solusi masih tersedia melalui endpoint polling (res.php). Implementasikan fallback yang melakukan polling untuk task apa pun yang tidak menerima callback dalam periode timeout.

Bisakah saya menggunakan mutual TLS (mTLS) untuk autentikasi callback?

Secara teori ya — tetapi sistem callback CaptchaAI menggunakan request HTTPS GET standar. HMAC signature memberikan autentikasi setara tanpa memerlukan manajemen sertifikat.


Artikel Terkait

Komentar dinonaktifkan untuk artikel ini.