Anwendungsfälle

CAPTCHA-Lösung für API-Endpunkttests in Webformularen

Ein CAPTCHA vor Ihrem Formular-Endpunkt darf Ihre Test-Suite nicht ausbremsen. Statt in jedem Testlauf einen kompletten Browser hochzufahren oder Tokens von Hand zu klicken, holen Sie das Token über die CaptchaAI-API, hängen es an die Payload und senden die Anfrage direkt an den Endpunkt. So prüfen Sie Backend-Validierung, Fehlerpfade und Rate-Limits reproduzierbar – headless, ohne UI-Automatisierung und schnell genug für CI.

Dieser Ansatz eignet sich für alle Fälle, in denen der eigentliche Prüfpunkt der Server ist und nicht das Rendering im Browser: reCAPTCHA v2, reCAPTCHA v3 und Cloudflare Turnstile decken den Großteil der Formulare ab, die deutschsprachige Teams testen.


Architektur des Testablaufs

┌──────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Solve    │────▶│ Build      │────▶│ POST to      │────▶│ Validate     │
│ CAPTCHA  │     │ Request    │     │ Endpoint     │     │ Response     │
│ (API)    │     │ Payload    │     │              │     │              │
└──────────┘     └────────────┘     └──────────────┘     └──────────────┘

Für die meisten Endpunkttests ist kein Browser erforderlich: Der Token-Anbieter kümmert sich um das CAPTCHA, der Tester baut die Anfrage und wertet die Antwort aus.


Wann sich dieser Ansatz lohnt

  • Backend-Validierung: Prüfen Sie, ob der Server CAPTCHA-Tokens tatsächlich verifiziert und nicht blind akzeptiert.
  • Lasttests: Senden Sie viele Anfragen an einen CAPTCHA-geschützten Endpunkt, ohne Browser-Instanzen zu skalieren.
  • Integrationstests in CI: Binden Sie die Formular-APIs in Ihre GitLab-CI- oder GitHub-Actions-Pipeline ein.
  • Fehlerpfade: Verifizieren Sie die korrekten Antworten auf ungültige und abgelaufene Tokens.

Voraussetzungen

Bevor Sie loslegen, brauchen Sie nur wenige Bausteine – kein Browser-Treiber, keine UI-Automatisierung:

  • CaptchaAI-API-Schlüssel – über das Dashboard auf captchaai.com verfügbar.
  • Python mit requests – die einzige externe Abhängigkeit im Beispiel.
  • sitekey und pageurl des zu testenden Formulars – beide stehen im HTML der Zielseite.
  • Eine Staging-Umgebung – testen Sie gegen eigene oder freigegebene Endpunkte, nicht gegen fremde Produktivsysteme.

Implementierung

CAPTCHA-Token über die API beziehen

Die Klasse TokenProvider kapselt die beiden Schritte des CaptchaAI-Workflows: Aufgabe an in.php übermitteln, anschließend das Ergebnis an res.php abfragen (Polling). Für reCAPTCHA v3 wird eine etwas längere Anfangswartezeit gesetzt, weil der Score-Mechanismus mehr Zeit braucht.

import time
import requests

class TokenProvider:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    def get_recaptcha_token(self, sitekey, pageurl, version="v2"):
        params = {
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
        }
        if version == "v3":
            params["version"] = "v3"
            params["action"] = "submit"
        return self._solve(params, initial_wait=15 if version == "v3" else 10)

    def get_turnstile_token(self, sitekey, pageurl):
        return self._solve({
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": pageurl,
        })

    def _solve(self, params, initial_wait=10):
        params["key"] = self.api_key
        params["json"] = 1
        resp = requests.post(f"{self.BASE}/in.php", data=params).json()
        if resp["status"] != 1:
            raise Exception(resp["request"])
        task_id = resp["request"]
        time.sleep(initial_wait)
        for _ in range(60):
            result = requests.get(
                f"{self.BASE}/res.php",
                params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
            ).json()
            if result["request"] == "CAPCHA_NOT_READY":
                time.sleep(5)
                continue
            if result["status"] == 1:
                return result["request"]
            raise Exception(result["request"])
        raise TimeoutError("Timed out")

Endpunkt-Tester aufbauen

Der EndpointTester löst das Token, hängt es unter dem passenden Feldnamen an die Payload und sendet die Anfrage. Neben dem Positivfall prüfen zwei eigene Methoden gezielt die Fehlerpfade – ungültiges Token und fehlendes Token –, denn genau dort scheitern reale Backends am häufigsten. Welcher Feldname zu welchem Typ gehört, fasst diese Tabelle zusammen:

CAPTCHA-Typ Wert für captcha_type Feldname (captcha_field)
reCAPTCHA v2 recaptcha_v2 g-recaptcha-response
reCAPTCHA v3 recaptcha_v3 g-recaptcha-response
Cloudflare Turnstile turnstile cf-turnstile-response
import json
import time

class EndpointTester:
    def __init__(self, api_key):
        self.token_provider = TokenProvider(api_key)
        self.session = requests.Session()
        self.results = []

    def test_endpoint(self, config):
        """
        config: {
            "name": "test name",
            "url": "endpoint URL",
            "method": "POST",
            "captcha_type": "recaptcha_v2" | "recaptcha_v3" | "turnstile",
            "sitekey": "...",
            "pageurl": "...",
            "captcha_field": "g-recaptcha-response",
            "payload": { ... form data ... },
            "expected_status": 200,
            "expected_contains": "success",
        }
        """
        start = time.time()
        result = {"name": config["name"], "passed": False}

        try:
            # Get CAPTCHA token
            captcha_type = config.get("captcha_type", "recaptcha_v2")
            if captcha_type == "recaptcha_v2":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"]
                )
            elif captcha_type == "recaptcha_v3":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"], version="v3"
                )
            elif captcha_type == "turnstile":
                token = self.token_provider.get_turnstile_token(
                    config["sitekey"], config["pageurl"]
                )
            else:
                raise ValueError(f"Unknown captcha type: {captcha_type}")

            # Build payload
            payload = {**config.get("payload", {})}
            captcha_field = config.get("captcha_field", "g-recaptcha-response")
            payload[captcha_field] = token

            # Submit request
            method = config.get("method", "POST").upper()
            headers = config.get("headers", {})

            if config.get("json_body"):
                resp = self.session.request(
                    method, config["url"], json=payload, headers=headers
                )
            else:
                resp = self.session.request(
                    method, config["url"], data=payload, headers=headers
                )

            # Validate response
            result["status_code"] = resp.status_code
            result["response_length"] = len(resp.text)
            result["elapsed"] = round(time.time() - start, 2)

            # Check expected status
            expected_status = config.get("expected_status", 200)
            if resp.status_code != expected_status:
                result["error"] = f"Expected {expected_status}, got {resp.status_code}"
                self.results.append(result)
                return result

            # Check expected content
            expected = config.get("expected_contains")
            if expected and expected.lower() not in resp.text.lower():
                result["error"] = f"Response missing: '{expected}'"
                self.results.append(result)
                return result

            result["passed"] = True

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_invalid_token(self, config):
        """Test that endpoint rejects invalid CAPTCHA tokens."""
        invalid_config = {**config}
        invalid_config["name"] = f"{config['name']} (invalid token)"

        # Override with fake token
        payload = {**config.get("payload", {})}
        captcha_field = config.get("captcha_field", "g-recaptcha-response")
        payload[captcha_field] = "INVALID_TOKEN_12345"

        start = time.time()
        result = {"name": invalid_config["name"], "passed": False}

        try:
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            # Should reject — 4xx or error message
            if resp.status_code >= 400 or "error" in resp.text.lower() or "invalid" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted invalid CAPTCHA token"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_missing_token(self, config):
        """Test that endpoint rejects missing CAPTCHA token."""
        start = time.time()
        result = {"name": f"{config['name']} (missing token)", "passed": False}

        try:
            payload = config.get("payload", {})
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            if resp.status_code >= 400 or "captcha" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted request without CAPTCHA"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def run_suite(self, configs):
        """Run a full test suite against multiple endpoints."""
        for config in configs:
            self.test_endpoint(config)
            self.test_invalid_token(config)
            self.test_missing_token(config)
        return self.report()

    def report(self):
        passed = sum(1 for r in self.results if r["passed"])
        total = len(self.results)
        lines = [f"Endpoint Tests: {passed}/{total} passed", "=" * 50]
        for r in self.results:
            status = "PASS" if r["passed"] else "FAIL"
            elapsed = r.get("elapsed", "?")
            lines.append(f"  [{status}] {r['name']} ({elapsed}s)")
            if r.get("error"):
                lines.append(f"         Error: {r['error']}")
        return "\n".join(lines)

Drei Prüfungen pro Endpunkt

Ein aussagekräftiger Endpunkt-Test besteht nicht aus einem einzelnen Aufruf, sondern aus drei ergänzenden Prüfungen. Genau diese fährt run_suite für jeden Eintrag automatisch:

  • Gültige Übermittlung: echtes Token von CaptchaAI, korrekte Payload – der Endpunkt soll 200 und die Erfolgsmeldung liefern.
  • Ungültiges Token: ein bewusst falscher Wert im CAPTCHA-Feld – der Endpunkt muss ablehnen, sonst validiert das Backend gar nicht.
  • Fehlendes Token: Anfrage komplett ohne CAPTCHA-Feld – der Endpunkt muss ebenfalls mit einem 4xx antworten.

Erst wenn alle drei Fälle das erwartete Verhalten zeigen, ist die CAPTCHA-Absicherung des Endpunkts belastbar getestet. Ein grüner Positivtest allein sagt nichts über die Sicherheit aus.


Anwendungsbeispiel

Ein typisches DACH-Szenario: Sie betreiben einen Shopware- oder JTL-Shop und wollen die Kontakt- und Newsletter-Endpunkte im Staging absichern, bevor eine Release-Pipeline sie live schaltet. Das folgende Beispiel testet zwei Endpunkte – ein Kontaktformular mit reCAPTCHA v2 und eine Newsletter-Anmeldung mit Cloudflare Turnstile. Ersetzen Sie die Beispiel-URLs durch Ihre eigenen Staging-Adressen; YOUR_API_KEY ist Ihr CaptchaAI-Schlüssel.

tester = EndpointTester("YOUR_API_KEY")

configs = [
    {
        "name": "Contact form submission",
        "url": "https://example.com/api/contact",
        "captcha_type": "recaptcha_v2",
        "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "pageurl": "https://example.com/contact",
        "captcha_field": "g-recaptcha-response",
        "payload": {
            "name": "Test User",
            "email": "test@example.com",
            "message": "Automated test message",
        },
        "expected_status": 200,
        "expected_contains": "success",
    },
    {
        "name": "Newsletter signup",
        "url": "https://example.com/api/subscribe",
        "captcha_type": "turnstile",
        "sitekey": "0x4AAAA...",
        "pageurl": "https://example.com/newsletter",
        "captcha_field": "cf-turnstile-response",
        "payload": {
            "email": "test@example.com",
        },
        "expected_status": 200,
    },
]

report = tester.run_suite(configs)
print(report)

Die run_suite-Methode fährt für jeden Endpunkt drei Prüfungen: gültige Übermittlung, ungültiges Token und fehlendes Token. Das Ergebnis zeigt kompakt, wo die Backend-Validierung greift – und wo nicht:

Ausgabe:

Endpoint Tests: 5/6 passed
==================================================
  [PASS] Contact form submission (18.5s)
  [PASS] Contact form submission (invalid token) (0.3s)
  [PASS] Contact form submission (missing token) (0.2s)
  [PASS] Newsletter signup (14.2s)
  [FAIL] Newsletter signup (invalid token) (0.3s)
         Error: Endpoint accepted invalid CAPTCHA token
  [PASS] Newsletter signup (missing token) (0.2s)

Der FAIL beim Newsletter-Endpunkt ist genau der Befund, für den solche Tests gebaut werden: Das Backend akzeptiert ein offensichtlich ungültiges Token – ein Sicherheitsproblem, das ein reiner Positivtest niemals aufdecken würde.

DSGVO-Hinweis: Sobald Ihr Test-Traffic personenbezogene Daten wie E-Mail- oder IP-Adressen erzeugt, greift die DSGVO. Verwenden Sie in Staging-Läufen synthetische Testdaten und keine echten Kundendatensätze.


Fehlerbehebung

Problem Ursache Lösung
Gültiges Token abgelehnt Token ist vor der Übermittlung abgelaufen Verzögerung zwischen Lösung und Absenden verkürzen
Ungültiges Token akzeptiert Backend validiert das CAPTCHA nicht Als Bug melden – ernstes Sicherheitsproblem
403 auf alle Anfragen CSRF-Token oder Cookies fehlen Session-Cookies oder CSRF-Header ergänzen
JSON-Endpunkt lehnt Formulardaten ab Falscher Content-Type json_body: True in der Konfiguration setzen

Häufige Fragen

Welche CAPTCHA-Typen deckt dieser Ansatz ab?

reCAPTCHA v2, reCAPTCHA v3 und Cloudflare Turnstile über die gezeigten Methoden. CaptchaAI löst darüber hinaus reCAPTCHA Enterprise, GeeTest v3, Cloudflare Challenge sowie Bild- und Raster-CAPTCHAs; hCaptcha und FunCaptcha werden nicht unterstützt.

Wie viel Zeit entfällt auf das Lösen des Tokens?

Den Großteil der Laufzeit macht das CAPTCHA aus – die HTTP-Anfrage und die Auswertung der Antwort dauern nur Millisekunden. Das zeigen die elapsed-Werte im Bericht: Die Positivtests mit echtem Token liegen deutlich über den Fehlerpfad-Tests ohne Lösung.

Wie integriere ich die Tests in GitLab CI oder GitHub Actions?

Hinterlegen Sie den API-Schlüssel als geschütztes CI-Secret und rufen Sie die Test-Suite in einem Job auf. Da kein Browser nötig ist, läuft alles in einem schlanken Python-Container – passend zu den in DACH verbreiteten GitLab-CI-Pipelines.

Welcher CaptchaAI-Tarif reicht für parallele Endpunkt-Tests?

Das richtet sich nach der Zahl gleichzeitiger Tests, denn CaptchaAI rechnet pro Thread ab – nicht pro Lösung. BASIC (15 $/Monat, 5 Threads) genügt für kleine Suites; für viele parallele Endpunkte bietet ADVANCE (90 $/Monat, 50 Threads) mehr Spielraum. Jeder Tarif enthält unbegrenzte Lösungen pro Thread.


Verwandte Leitfäden


Sichern Sie jeden CAPTCHA-geschützten Endpunkt in Ihrer Pipeline ab – mit CaptchaAI.

Kommentare sind für diesen Artikel deaktiviert.