Ein CaptchaAI-Callback-Endpunkt ist eine öffentlich erreichbare URL – und genau das macht ihn angreifbar. Sobald Sie die pingback-Funktion nutzen, schickt CaptchaAI die fertige CAPTCHA-Lösung per HTTP an Ihren Server. Nimmt Ihr Endpunkt jede eingehende Anfrage ungeprüft an, kann jeder, der die URL errät, gefälschte Lösungen einspielen. Die Absicherung besteht aus vier Prüfschichten: Task-ID-Abgleich, HMAC-Signatur, IP-Allowlist und Replay-Schutz. Dieses Tutorial zeigt jede Schicht mit lauffähigem Python- und Node.js-Code.
Warum ein Callback-Endpunkt ein lohnendes Angriffsziel ist
Beim Polling-Modell fragt Ihr Code aktiv den Status ab (res.php) – die Antwort kommt nur auf Ihre eigene, authentifizierte Anfrage. Der Callback dreht diese Richtung um: Ein fremder Server ruft Ihren Endpunkt auf. Damit verschiebt sich die Verantwortung. Nicht CaptchaAI muss sich bei Ihnen ausweisen, sondern Sie müssen entscheiden, ob eine eingehende Anfrage vertrauenswürdig ist.
Das ist heikel, weil die Callback-URL an vielen Stellen sichtbar wird:
- in Server- und Proxy-Logs,
- in Fehlermeldungen und Stack-Traces,
- in der Netzwerküberwachung,
- in einem versehentlich geleakten Konfigurationsfile.
Wer die URL kennt, kann eine GET-Anfrage mit beliebigen id- und code-Parametern absetzen. Ohne Prüfung landet dieser gefälschte Token in Ihrer Verarbeitung – und Ihre Automatisierung sendet ihn womöglich an das Zielformular weiter. Die folgenden Schichten schließen genau diese Lücke, von der minimalen Pflichtprüfung bis zum Schutz vor wiederholten Anfragen.
So läuft der CaptchaAI-Callback ab
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
Der Knackpunkt ist Schritt 3: Diese GET-Anfrage trägt keinerlei Authentifizierung. Ihr Endpunkt muss selbst feststellen, ob sie wirklich von CaptchaAI stammt. Vier Schichten sichern ihn ab:
- Task-ID-Abgleich – nur selbst übermittelte IDs akzeptieren.
- HMAC-Signatur – die Callback-URL kryptografisch signieren.
- IP-Allowlist – nur bekannte Absender-IPs zulassen.
- Replay-Schutz – jeden Callback genau einmal verarbeiten.
Die Schichten bauen aufeinander auf – kombinieren Sie sie, statt sich auf eine einzelne zu verlassen.
Schicht 1: Task-ID abgleichen
Die Pflichtprüfung mit dem geringsten Aufwand: Akzeptieren Sie Callback-Ergebnisse nur für Task-IDs, die Sie tatsächlich selbst übermittelt haben. Jede unbekannte ID wird sofort mit 403 abgewiesen. Speichern Sie die offenen IDs in einer Thread-sicheren Struktur, damit parallele Worker sich nicht in die Quere kommen.
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);
Diese Prüfung allein hält bereits jede Anfrage ab, deren id nicht aus Ihrer eigenen Übermittlung stammt. Zwei Punkte in der Umsetzung sind wichtig:
- Speichern Sie offene IDs Thread-sicher (
pending_lock), sonst kollidieren parallele Worker beim Zugriff. - Betreiben Sie mehrere Instanzen, halten Sie die IDs zentral in Redis vor – so wird ein Callback auch dann erkannt, wenn ihn eine andere Instanz entgegennimmt.
Schicht 2: HMAC-Signatur im Callback-Token
Task-IDs sind mitunter kurz und vorhersehbar. Ergänzen Sie deshalb ein Geheimnis, das ein Angreifer nicht raten kann: Sie berechnen aus der Task-ID und einem CALLBACK_SECRET eine HMAC-SHA256-Signatur und hängen sie als token-Parameter an die Callback-URL. Im Endpunkt berechnen Sie die Signatur erneut und vergleichen sie zeitkonstant.
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);
});
Übergeben Sie beim Absenden die signierte URL: pingback=https://your-server.com/captcha/callback?token=HMAC_SIGNATURE. Zwei Details entscheiden hier über die Sicherheit:
- Nutzen Sie den zeitkonstanten Vergleich
hmac.compare_digestbeziehungsweisecrypto.timingSafeEqual; ein naiver==-Vergleich verrät über Laufzeitunterschiede Stück für Stück die korrekte Signatur. - Halten Sie
CALLBACK_SECRETausschließlich in einer Umgebungsvariablen – nie im Quellcode und nie in der URL-Historie.
Schicht 3: IP-Allowlist für CaptchaAI-Server
Als zusätzliche Hürde beschränken Sie den Endpunkt auf die bekannten Absender-IPs von CaptchaAI. Wichtig zur Einordnung:
- Die IP-Allowlist ersetzt die Signaturprüfung nicht – IPs lassen sich fälschen, Signaturen nicht.
- Sie filtert aber den Großteil des unerwünschten Traffics ab, bevor er überhaupt Rechenzeit kostet.
Python (Flask)
# CaptchaAI callback source IPs (verify current IPs mit 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();
});
Hinweis: Die aktuelle Liste der Callback-Quell-IPs erhalten Sie beim CaptchaAI-Support. Betreiben Sie den Endpunkt hinter einem Reverse-Proxy – etwa auf einem Server bei Hetzner oder IONOS mit vorgelagertem nginx –, müssen Sie den Header
X-Forwarded-Forkorrekt auswerten, sonst sehen Sie nur die IP des Proxys statt der echten Absender-IP.
Replay-Angriffe verhindern
Selbst ein gültiger Callback lässt sich mitschneiden und erneut abspielen. Ergänzen Sie deshalb eine Zeitstempelprüfung und erzwingen Sie die Einmalverwendung: Callbacks, die älter als das TTL-Fenster sind, werden verworfen, und jede Task-ID wird nur einmal verarbeitet.
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
Der Endpunkt kombiniert dafür zwei Mechanismen:
- Zeitstempelprüfung – Callbacks außerhalb des TTL-Fensters (
CALLBACK_TTL) gelten als abgelaufen. - Einmalverwendung – jede bereits gesehene Task-ID wird mit
409abgewiesen.
In der Praxis wächst die Menge used_callbacks unbegrenzt – legen Sie sie in einem Store mit Ablaufzeit ab, etwa als Redis-Schlüssel mit einer TTL, die dem Callback-Fenster entspricht. So bleibt der Replay-Schutz auch über mehrere Prozesse hinweg konsistent.
Sicherheits-Checkliste im Überblick
| Schicht | Schützt vor | Umsetzung |
|---|---|---|
| Task-ID-Abgleich | Injektion unbekannter Task-IDs | Offene IDs speichern, unbekannte ablehnen |
| HMAC-Signatur | Erraten der URL, gefälschte Callbacks | Callback-URL mit Secret signieren |
| IP-Allowlist | Anfragen von nicht autorisierten Servern | Bekannte CaptchaAI-IPs freigeben |
| Replay-Schutz | Erneut eingespielte gültige Callbacks | Einmalverwendung + Zeitstempelprüfung |
| HTTPS | Mitlesen, Man-in-the-Middle | TLS am Callback-Endpunkt erzwingen |
Behandeln Sie die ersten beiden Schichten als Pflicht, die dritte als sinnvolle Ergänzung und den Replay-Schutz als Muss, sobald am Ende der Kette eine Zahlung, ein Login oder ein anderer sensibler Vorgang steht.
Häufige Fragen
Wie lang muss das Callback-Secret für HMAC sein?
Mindestens 32 zufällige Zeichen aus einem kryptografisch sicheren Generator (secrets.token_urlsafe in Python, crypto.randomBytes in Node.js). Kürzere oder aus Wörtern zusammengesetzte Secrets lassen sich per Brute Force angreifen. Legen Sie das Secret nur als Umgebungsvariable ab, niemals im Code.
Was passiert, wenn mein Endpunkt beim Callback nicht erreichbar ist?
Die Lösung bleibt über das Polling verfügbar: Fragen Sie res.php mit der Task-ID als Fallback ab. Bauen Sie einen Watchdog, der Aufgaben ohne Callback nach einem Zeitfenster aktiv nachlädt, statt sich allein auf den eingehenden Ping zu verlassen.
Warum reicht HTTPS am Endpunkt nicht als alleinige Absicherung?
HTTPS schützt den Transportweg gegen Mitlesen und Manipulation, sagt aber nichts über die Herkunft der Anfrage aus. Ein Angreifer kann jederzeit selbst eine HTTPS-GET-Anfrage an Ihre URL senden. Erst Task-ID-Abgleich und HMAC-Signatur belegen, dass der Callback wirklich zu einer Ihrer Übermittlungen gehört.
Zählen die gefilterten Client-IPs als personenbezogene Daten nach DSGVO?
Ja – IP-Adressen gelten nach DSGVO in der Regel als personenbezogene Daten. Für die Allowlist heißt das:
- Zweck und Aufbewahrungsdauer der geloggten IPs dokumentieren.
- Rechtsgrundlage der Verarbeitung prüfen.
Das betrifft Ihre eigene Infrastruktur, nicht die CAPTCHA-Lösung selbst.
Fehlerbehebung
Diese Fälle treten beim Einrichten der Callback-Validierung am häufigsten auf – jeweils mit Ursache und konkreter Lösung:
| Problem | Ursache | Lösung |
|---|---|---|
| Alle Callbacks werden abgelehnt | IP-Allowlist enthält die CaptchaAI-IPs nicht | Aktuelle IPs beim Support erfragen; X-Forwarded-For hinter dem Proxy prüfen |
| HMAC-Prüfung schlägt fehl | Task-ID bei Übermittlung und Callback weichen ab | Exakt die von in.php zurückgegebene Task-ID signieren |
| Doppelte Callbacks in der Verarbeitung | Race Condition bei parallelen Callbacks | Atomare Set-Operationen oder Unique-Constraint in der Datenbank |
| Callbacks laufen ins Timeout | Endpunkt antwortet zu langsam | Sofort mit 200 quittieren, Verarbeitung asynchron im Hintergrund |
Verwandte Leitfäden
- Callback-Fehler abfangen: erneuter Versuch und Dead-Letter-Queue
- CaptchaAI-Zugangsdaten sicher speichern
- IP-Whitelisting und API-Schlüssel absichern