Tutorials

CaptchaAI Webhook Güvenliği: Callback İmzalarını Doğrulama

Kısa yanıt: bir callback uç noktasını tek bir kontrolle değil, üst üste binen dört katmanla güvenceye alırsınız — görev kimliği (task ID) doğrulaması, HMAC imzası, IP izin listesi ve replay koruması. CaptchaAI'nin pingback özelliğini kullandığınızda sunucunuz, CAPTCHA çözümlerini karşılayan herkese açık bir HTTP uç noktası yayınlar. Türkiye'deki e-ticaret ve fintech ekiplerinde bu uç nokta çoğu zaman ödeme veya giriş akışlarının QA testlerinde tetiklenir; doğrulama olmadan bıraktığınızda ise URL'yi ele geçiren biri sahte çözümler enjekte edebilir ve KVKK kapsamındaki verileri riske atabilir. Doğrulamasız bir uç noktada karşılaştığınız somut riskler şunlardır:

  • Sahte çözüm enjeksiyonu: URL'yi keşfeden biri, uç noktanızı geçersiz token'larla doldurabilir.
  • Yanlış görev eşleştirmesi: hiç göndermediğiniz task ID'lere ait sonuçlar veri tablonuzu kirletir.
  • Tekrar (replay): ağ üzerinde yakalanan geçerli bir callback yeniden gönderilerek çift işlem yaratır.

Callback akışı nasıl çalışır?

Tipik akış üç adımdan oluşur:


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

Zayıf nokta 3. adımdır: bu istek kimliği doğrulanmamıştır. İsteğin gerçekten CaptchaAI'den mi, yoksa uç noktanızı tarayan birinden mi geldiğini ayırt etmeniz gerekir. Aşağıdaki dört katman tam olarak bunu sağlar.

Dört savunma katmanına genel bakış

  1. Görev kimliği doğrulaması — yalnızca sizin gönderdiğiniz task ID'lere ait callback'leri kabul edin.
  2. HMAC imzası — callback URL'sini gizli bir anahtarla imzalayıp uç noktada doğrulayın.
  3. IP izin listesi — yalnızca CaptchaAI'nin sunucu IP'lerinden gelen isteklere izin verin.
  4. Replay koruması — zaman damgası ve tek kullanımlık yaptırımla tekrar gönderimi engelleyin.

Strateji 1: Görev kimliği (task ID) doğrulaması

En düşük maliyetli katmanla başlayın: gönderdiğiniz her task ID'yi bir kümede tutun, callback geldiğinde ID kümede yoksa 403 döndürün.

Python (Flask)

Aşağıdaki Flask uygulaması, gönderdiği her task ID'yi bir kümede tutar ve callback'te bu kimliği doğrular:

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)

Aynı mantığın Express karşılığı, bekleyen ID'leri bir Set içinde tutar:

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

Bu katman şunları durdurur:

  • Rastgele veya tahmin edilen task ID'lerle yapılan enjeksiyonu.
  • Hiç göndermediğiniz görevlere ait sahte sonuçları.
  • Sınır: URL'yi ve geçerli bir ID'yi birlikte ele geçiren saldırganı tek başına durduramaz — bunun için Strateji 2'yi ekleyin.

Strateji 2: HMAC imza token'ı

Task ID doğrulaması rastgele denemeleri eler, ama URL'yi gören biri geçerli bir ID'yi de görmüş olabilir. Bu katman, callback URL'sine tahmin edilemez bir imza ekler: gizli bir anahtarla task ID'yi imzalar, uç noktada aynı imzayı yeniden hesaplayıp sabit zamanlı karşılaştırmayla doğrularsınız.

Python

Aşağıdaki fonksiyon imzalı URL üretir; callback tarafında imza yeniden hesaplanıp hmac.compare_digest ile karşılaştırılır:

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

Node.js tarafında crypto modülüyle timingSafeEqual üzerinden aynı doğrulama:

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

İmza katmanını doğru işletmek için:

  • Görevi gönderirken imzalı URL'yi kullanın: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE.
  • Gizli anahtarı koda gömmeyin; ortam değişkeninde saklayın.
  • Anahtar en az 32 karakter rastgele olsun ve sızdığından şüphelenirseniz hemen döndürün.

Strateji 3: IP izin listesi (allowlist)

Üçüncü katman ağ seviyesinde çalışır: callback uç noktanızı yalnızca CaptchaAI'nin sunucu IP'lerinden gelen isteklere açın. Bu, geçerli bir imza olmadan gelen trafiği daha uç noktaya varmadan reddeder.

Python (Flask)

Flask'ta bir before_request kancasıyla istemci IP'sini filtreleyin:

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

Express'te aynı filtre bir ara katman (middleware) olarak eklenir:

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

Not: Güncel callback kaynak IP'leri listesi için CaptchaAI desteğine başvurun. Ters proxy arkasındaysanız X-Forwarded-For başlıklarının doğru yapılandırıldığından emin olun — aksi halde gerçek istemci IP'si yerine proxy'nin IP'sini kontrol edersiniz.

Bu katmanı ne zaman eklemelisiniz:

  • CaptchaAI kararlı callback IP'leri yayınladığında en etkilidir.
  • Tek başına kimlik doğrulama değil, HMAC imzasını tamamlayan bir ağ katmanıdır.

Tekrar (replay) saldırılarını önleme

Geçerli bir callback bile ağ üzerinde yakalanıp yeniden gönderilebilir. Özellikle ödeme onayı gibi finansal iş akışlarında — Türkiye'deki e-ticaret checkout QA senaryolarında sık karşılaşılır — aynı çözümün iki kez işlenmesi çift kayda yol açar. İki basit kontrol bunu engeller:

  • Zaman damgası (ts) tazeliğini kontrol edin: belirli bir süreden eski callback'leri reddedin.
  • Her task ID'yi yalnızca bir kez işleyin; ikinci kez gelirse 409 döndürün.

Aşağıdaki uç nokta her iki kontrolü de uygular:

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

Katmanlı güvenlik kontrol listesi

Dört katmanı birlikte uyguladığınızda savunma şöyle görünür:

Katman Neye karşı korur Uygulama
Görev kimliği doğrulaması Rastgele/bilinmeyen görev enjeksiyonu Bekleyen ID'leri saklayın, bilinmeyeni reddedin
HMAC imzası URL tahmini, sahte callback'ler Callback URL'sini gizli anahtarla imzalayın
IP izin listesi Yetkisiz sunuculardan gelen istekler Yalnızca CaptchaAI IP'lerine izin verin
Replay koruması Yeniden gönderilen geçerli callback'ler Tek kullanım + zaman damgası doğrulaması
HTTPS Dinleme, ortadaki adam (MITM) Callback uç noktasında TLS

Sorun giderme

Sorun Sebep Düzeltme
Tüm callback'ler reddediliyor IP izin listesi CaptchaAI IP'lerini içermiyor Güncel IP'leri destekle doğrulayın; ters proxy başlıklarını kontrol edin
HMAC doğrulaması başarısız oluyor Gönderim ve callback arasında task ID uyuşmazlığı in.php'nin döndürdüğü task ID'yi birebir kullanın
Yinelenen callback'ler işleniyor Eşzamanlı callback'lerde yarış durumu Atomik küme işlemleri veya veritabanı benzersizlik kısıtı kullanın
Callback'ler zaman aşımına uğruyor Uç nokta yanıtı çok geç veriyor Eşzamansız işleyin — hemen kabul edin, arka planda işleyin

Sık sorulan sorular

Callback güvenliğini uygularken en sık karşılaşılan sorular:

HMAC gizli anahtarını nerede saklamalıyım?

Kod deposunda değil, ortam değişkeninde veya bir secret yöneticisinde tutun. Anahtar en az 32 karakter rastgele olmalı ve sızdığından şüphelenirseniz hemen döndürülmelidir; imza güvenliğinin tamamı bu anahtarın gizli kalmasına dayanır.

IP izin listesi tek başına yeterli mi?

Hayır. IP'ler zamanla değişebilir ve ters proxy arkasında X-Forwarded-For ile taklit edilebilir. IP izin listesini tamamlayıcı bir ağ katmanı olarak görün; birincil kimlik doğrulamayı HMAC imzasıyla yapın.

İmza doğrulaması callback yanıtını yavaşlatır mı?

Fark edilmeyecek kadar az. HMAC-SHA256 hesaplaması mikrosaniyeler sürer. Asıl gecikme çözümü senkron işlemekten gelir — callback'i hemen 200 ile kabul edip ağır işi arka plana alın.

Callback uç noktam yanıt vermezse çözümü kaybeder miyim?

Hayır. Çözüm res.php sorgulama uç noktasında hâlâ durur. Belirli bir süre içinde callback almayan görevleri periyodik olarak sorgulayan bir yedek mekanizma kurun.

İlgili Makaleler

Sonraki Adımlar

CaptchaAI callback uç noktalarınızı güvenceye alın — API anahtarınızı alın ve imza doğrulamasını bugün ekleyin. İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.