Tutorials

CAPTCHA in Pop-up-Modalen: Erkennung und Token-Injektion

Modale CAPTCHAs verhalten sich anders als statische Widgets: Beim Laden der Seite existiert weder der Sitekey noch das CAPTCHA-Element im DOM. Beides entsteht erst, wenn eine Aktion den Dialog öffnet – ein Login-Klick, ein abgesendetes Formular oder ein Rate-Limit. Für die Automatisierung bedeutet das eine feste Reihenfolge: Dialog gezielt auslösen, auf das Rendern des Widgets warten, den Sitekey frisch aus dem sichtbaren Modal auslesen, die Abfrage über CaptchaAI lösen und das Token einfügen, bevor der Dialog wieder schließt. Dieser Leitfaden zeigt den vollständigen Ablauf mit Playwright und Puppeteer für reCAPTCHA v2 und Cloudflare Turnstile.

Warum modale CAPTCHAs mehr Sorgfalt verlangen

Der Unterschied zu einem CAPTCHA im statischen Seiten-HTML ist das Timing. Ein modaler Dialog bringt drei zusätzliche Fehlerquellen mit: Das Widget rendert erst mit Verzögerung, der Dialog kann sich selbst wieder schließen, und die Serversitzung kann ablaufen, während Ihr Skript auf das Lösungsergebnis wartet. Wer den Sitekey zu früh ausliest, erhält null; wer zu spät einfügt, arbeitet mit einem bereits abgelaufenen Token. Ein robuster Handler synchronisiert deshalb jeden Schritt an einem beobachtbaren Zustand des Dialogs – nicht an festen Wartezeiten allein.

Typische Auslöser für modale CAPTCHA-Abfragen

Muster Auslöser Verhalten
Login-Dialog Klick auf „Anmelden" CAPTCHA lädt im Overlay-Div
Anti-Bot-Interstitial Automatisch bei auffälligem Verhalten Vollbild-Dialog blockiert die Seite
Checkout-Bestätigung Absenden des Zahlungsformulars Dialog erscheint zur Verifizierung
Rate-Limit-Hinweis Zu viele Anfragen erkannt Dialog mit CAPTCHA-Gate
Cookie-Zustimmung + CAPTCHA Erster Besuch CAPTCHA im Zustimmungsdialog eingebettet

Der Ablauf in fünf Schritten

Bevor Sie den Code lesen, hilft die Vogelperspektive. Der Handler durchläuft immer dieselbe Kette: Erstens den Dialog auslösen, sofern er nicht ohnehin automatisch erscheint. Zweitens auf ein sichtbares Modal-Element warten, statt auf einen festen Timer zu vertrauen. Drittens den Sitekey und den CAPTCHA-Typ direkt aus dem Modal-DOM auslesen. Viertens die Abfrage an CaptchaAI übermitteln und das Ergebnis abfragen. Fünftens das Token in das passende Antwortfeld eintragen und – falls vorhanden – den Callback auslösen, damit der Dialog die Lösung akzeptiert. Neu bei CaptchaAI? Der Schnellstart-Leitfaden am Ende dieses Artikels führt durch API-Schlüssel und erste Anfrage.

Python: modale CAPTCHAs mit Playwright lösen

Das folgende Skript kapselt die gesamte Kette in handle_modal_captcha: Trigger, Warten auf das Modal, Erkennung, Lösung und Einfügen des Tokens laufen in einem Aufruf ab.

import requests
import time
from playwright.sync_api import sync_playwright

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
    """Submit and poll a CAPTCHA."""
    params = {
        "key": API_KEY,
        "method": method,
        "json": 1,
    }
    if method == "userrecaptcha":
        params["googlekey"] = sitekey
        params["pageurl"] = pageurl
    elif method == "turnstile":
        params["sitekey"] = sitekey
        params["pageurl"] = pageurl

    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(60):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")


def detect_modal_captcha(page):
    """
    Detect CAPTCHA inside a visible modal/dialog.
    Returns (sitekey, method) or (None, None).
    """
    return page.evaluate("""
        () => {
            // Find visible modals
            const modalSelectors = [
                '.modal.show',
                '.modal[style*="display: block"]',
                'dialog[open]',
                '[role="dialog"]:not([aria-hidden="true"])',
                '.overlay.visible',
                '.popup.active',
                '[class*="modal"][class*="open"]',
            ];

            let modal = null;
            for (const sel of modalSelectors) {
                const el = document.querySelector(sel);
                if (el && el.offsetParent !== null) {
                    modal = el;
                    break;
                }
            }

            // If no modal found, search entire document
            const searchRoot = modal || document;

            // Check for reCAPTCHA
            const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
            if (recaptcha) {
                return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
            }

            // Check for Turnstile
            const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
            if (turnstile) {
                return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
            }

            // Check for hCaptcha
            const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
            if (hcaptcha) {
                return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
            }

            return null;
        }
    """)


def inject_token_in_modal(page, token, method="userrecaptcha"):
    """Inject token into the CAPTCHA inside the modal."""
    if method == "userrecaptcha":
        page.evaluate("""
            (token) => {
                // Find response textarea (may be inside modal)
                const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
                textareas.forEach(ta => {
                    ta.value = token;
                    ta.style.display = 'block';
                });

                // Trigger callback
                if (typeof ___grecaptcha_cfg !== 'undefined') {
                    Object.values(___grecaptcha_cfg.clients).forEach(client => {
                        Object.values(client).forEach(val => {
                            if (val && typeof val === 'object') {
                                Object.values(val).forEach(v => {
                                    if (v && typeof v.callback === 'function') v.callback(token);
                                });
                            }
                        });
                    });
                }
            }
        """, token)
    elif method == "turnstile":
        page.evaluate("""
            (token) => {
                const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
                inputs.forEach(inp => { inp.value = token; });

                if (typeof window.turnstileCallback === 'function') {
                    window.turnstileCallback(token);
                }
            }
        """, token)


def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
    """
    Full workflow: trigger modal, detect CAPTCHA, solve, inject.
    """
    # Step 1: Trigger the modal if needed
    if trigger_selector:
        print(f"Clicking trigger: {trigger_selector}")
        page.click(trigger_selector)

    # Step 2: Wait for modal to become visible
    print("Waiting for modal...")
    modal_selectors = [
        ".modal.show",
        "dialog[open]",
        '[role="dialog"]:not([aria-hidden="true"])',
        ".popup.active",
    ]

    modal_visible = False
    for selector in modal_selectors:
        try:
            page.wait_for_selector(selector, timeout=timeout)
            modal_visible = True
            print(f"  Modal detected: {selector}")
            break
        except Exception:
            continue

    if not modal_visible:
        print("  No modal detected")
        return None

    # Step 3: Wait for CAPTCHA to render inside modal
    time.sleep(2)  # Brief pause for dynamic CAPTCHA loading

    captcha_info = detect_modal_captcha(page)
    if not captcha_info:
        print("  No CAPTCHA found in modal")
        return None

    sitekey = captcha_info["sitekey"]
    method = captcha_info["method"]
    print(f"  Found {method} CAPTCHA: {sitekey[:20]}...")

    # Step 4: Solve via CaptchaAI
    token = solve_captcha(sitekey, page.url, method)
    print(f"  Solved: {token[:30]}...")

    # Step 5: Inject token
    inject_token_in_modal(page, token, method)
    print("  Token injected")

    return token


def main():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        page.goto("https://example.com")
        page.wait_for_load_state("networkidle")

        # Handle CAPTCHA that appears in login modal
        token = handle_modal_captcha(
            page,
            trigger_selector="button#login-btn",
            timeout=10000,
        )

        if token:
            # Fill form fields inside modal
            page.fill('dialog input[name="email"]', "user@example.com")
            page.fill('dialog input[name="password"]', "password123")

            # Submit modal form
            page.click('dialog button[type="submit"]')
            page.wait_for_load_state("networkidle")

        browser.close()


main()

JavaScript: Puppeteer-Handler für modale Dialoge

Für Node.js-Stacks bildet Puppeteer denselben Ablauf ab. waitForModal prüft die Selektoren der Reihe nach, detectModalCaptcha liest Sitekey und Typ, und der abschließende page.evaluate-Aufruf trägt das Token in das passende Feld ein.

const puppeteer = require("puppeteer");

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
  const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
  if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
  else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", pageurl); }

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function waitForModal(page, timeout = 10000) {
  const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
  for (const sel of selectors) {
    try {
      await page.waitForSelector(sel, { visible: true, timeout });
      return sel;
    } catch {}
  }
  return null;
}

async function detectModalCaptcha(page) {
  return page.evaluate(() => {
    const checks = [
      { sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
      { sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
    ];
    for (const { sel, method } of checks) {
      const el = document.querySelector(sel);
      if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
    }
    return null;
  });
}

async function handleModalCaptcha(page, triggerSelector) {
  // Trigger modal
  if (triggerSelector) await page.click(triggerSelector);

  // Wait for modal
  const modalSel = await waitForModal(page);
  if (!modalSel) { console.log("No modal found"); return null; }
  console.log(`Modal visible: ${modalSel}`);

  // Wait for CAPTCHA render
  await new Promise((r) => setTimeout(r, 2000));

  const info = await detectModalCaptcha(page);
  if (!info) { console.log("No CAPTCHA in modal"); return null; }

  console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
  const token = await solveCaptcha(info.sitekey, page.url(), info.method);
  console.log(`Solved: ${token.substring(0, 30)}...`);

  // Inject token
  await page.evaluate((t, method) => {
    if (method === "userrecaptcha") {
      document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
    } else if (method === "turnstile") {
      document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
    }
  }, token, info.method);

  return token;
}

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto("https://example.com", { waitUntil: "networkidle2" });

  const token = await handleModalCaptcha(page, "button#login-btn");
  if (token) {
    await page.type('dialog input[name="email"]', "user@example.com");
    await page.click('dialog button[type="submit"]');
    await page.waitForNavigation();
  }

  await browser.close();
})();

Timing im Modal richtig steuern

Die häufigsten Ausfälle sind keine Solver-Fehler, sondern Timing-Fehler. Diese vier Faktoren entscheiden, ob das Token rechtzeitig ankommt:

Faktor Auswirkung Gegenmaßnahme
Automatisches Schließen des Modals Der Dialog schließt, bevor die Lösung vorliegt Sofort nach der Erkennung mit dem Lösen beginnen
Sitzungsablauf während des Lösens Die Serversitzung läuft im Wartezustand ab Sitzung per Hintergrund-Heartbeat aktiv halten
Render-Verzögerung im Dialog Das Widget lädt im Modal 1–3 Sekunden verzögert Nach dem Sichtbarwerden 2 Sekunden warten, dann den Sitekey auslesen
Token-Ablauf beim Ausfüllen Das Token verfällt, während Sie das Formular befüllen CAPTCHA zuletzt lösen – nach allen übrigen Feldern

Häufige Probleme und ihre Ursachen

Problem Ursache Lösung
Token wird erzeugt, aber vom Ziel abgelehnt sitekey, pageurl oder Session-Kontext passen nicht Parameter frisch erfassen und das Token in derselben Browser- oder HTTP-Sitzung verwenden
Modal erkannt, aber kein CAPTCHA gefunden Das Widget lädt asynchron nach dem Öffnen des Dialogs Wartezeit erhöhen oder mit MutationObserver auf das Einfügen des Widgets reagieren
Token eingefügt, aber Dialog schließt nicht Die Callback-Funktion wurde nicht ausgelöst Den CAPTCHA-Callback gezielt suchen und explizit aufrufen
Sitekey ändert sich bei jedem Aufruf Der Dialog erzeugt dynamische CAPTCHA-Instanzen Sitekey immer frisch aus dem DOM des Modals lesen, niemals zwischenspeichern
Polling läuft ins Timeout Intervall oder Fehlerbehandlung sind zu eng gesetzt Alle 5–10 Sekunden abfragen, Timeout von echten Fehlercodes trennen und die Ursache loggen

Modale CAPTCHAs im eigenen Test-Setup

In DACH-Teams laufen End-to-End-Tests häufig in GitLab CI, das in deutschen Unternehmen neben GitHub Actions stark verbreitet ist. Sobald ein Login- oder Checkout-Dialog eine CAPTCHA-Abfrage einblendet, bleibt die Pipeline stehen. Der hier gezeigte Handler löst das für QA in eigenen oder autorisierten Staging-Umgebungen – etwa beim Testen eines Shopware- oder JTL-Checkouts auf https://staging.example-app.test, mit Test-Zahlungsdaten statt echter Transaktionen. Ein Hinweis zur Sorgfalt: Sobald Sie beim Scraping IP-Adressen oder Nutzerdaten verarbeiten, gelten diese nach DSGVO als personenbezogene Daten. Prüfen Sie Ihre Rechtsgrundlage und Datenflüsse, bevor der Workflow produktiv geht.

Häufige Fragen

Welche CAPTCHA-Typen löst CaptchaAI in modalen Dialogen?

CaptchaAI löst reCAPTCHA v2 (inklusive Invisible und Callback), reCAPTCHA v3, Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild- und Raster-CAPTCHAs – unabhängig davon, ob sie direkt auf der Seite oder erst in einem Modal erscheinen. hCaptcha und FunCaptcha werden nicht unterstützt; GeeTest v4 ist als „bald verfügbar" angekündigt. Details zur Integration finden Sie im verlinkten Leitfaden zu reCAPTCHA v2.

Wie lange bleibt ein gelöstes Token gültig?

Meist nur rund 120 Sekunden. reCAPTCHA- und Turnstile-Tokens verfallen kurz nach der Ausstellung. Lösen Sie das CAPTCHA deshalb als letzten Schritt vor dem Absenden und fügen Sie das Token sofort ein, statt es zwischenzuspeichern.

Warum akzeptiert die Zielseite mein eingefügtes Token nicht?

Fast immer, weil Sitekey, pageurl oder der Session-Kontext nicht zur Anfrage passen. Lesen Sie den Sitekey frisch aus dem sichtbaren Modal, übergeben Sie die exakte pageurl und verwenden Sie das Token in derselben Sitzung, in der es angefordert wurde. Die verlinkten API-Antwortformate und Fehlercodes helfen bei der genauen Ursache.

Was kostet das Lösen modaler CAPTCHAs?

CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Der Einstieg BASIC kostet 15 $/Monat und umfasst 5 Threads mit unbegrenzten Lösungen. Ein Modal-Workflow belegt einen Thread nur für die Dauer einer einzelnen Lösung. Preise in US-Dollar.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.