Explainers

Extraire les URL anchor et bframe de reCAPTCHA

Vous n'avez presque jamais besoin des URL anchor et bframe : l'API CaptchaAI se contente du sitekey et du pageurl. Elles servent le jour où quelque chose cloche — token refusé sans motif, sitekey injecté en JavaScript, deux widgets sur la même page.

reCAPTCHA v2 s'affiche dans deux iframes imbriquées : l'anchor porte la case à cocher, la bframe la grille d'images. Leurs URL exposent le sitekey, l'origine encodée, la version du bundle et la langue du défi.


Les deux iframes que reCAPTCHA charge réellement

Google injecte deux iframes servies depuis www.google.com. La première est toujours là ; la seconde n'apparaît que si le clic déclenche un défi.

Target page (example.com/login)
    └── <iframe src="https://www.google.com/recaptcha/api2/anchor?...">
        │   ← Anchor iframe: "I'm not a robot" checkbox
        │
        └── <iframe src="https://www.google.com/recaptcha/api2/bframe?...">
                ← Bframe iframe: Image challenge grid (loads when clicked)

L'iframe anchor : le widget et le premier verdict

Elle porte la case à cocher et l'analyse de risque qui arbitre entre passage direct et grille d'images. Son URL porte toute la configuration :

https://www.google.com/recaptcha/api2/anchor?
    ar=1
    &k=6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp  ← site key
    &co=aHR0cHM6Ly9leGFtcGxlLmNvbTo0NDM.           ← encoded origin
    &hl=en                                           ← language
    &v=jF2Zb_rr_5sv8dMHoGIn-XxY                    ← reCAPTCHA version
    &size=normal                                     ← widget size
    &cb=89fu2pf0swif                                ← callback ID

L'iframe bframe : la grille d'images

Créée seulement quand le défi se déclenche, son URL se reconstruit à partir de trois paramètres de l'anchor :

https://www.google.com/recaptcha/api2/bframe?
    hl=en
    &v=jF2Zb_rr_5sv8dMHoGIn-XxY
    &k=6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp

Les paramètres de l'URL anchor

Paramètre Rôle Ce qu'il vous apprend
k Clé du site La clé réellement chargée par la page
co Origine encodée Protocole, domaine et port, en base64
v Version Hash du bundle JavaScript reCAPTCHA
hl Langue Langue du défi (fr, en…)
size Taille normal, compact ou invisible
cb Callback Identifiant de la fonction de callback
theme Thème light ou dark
ar Ratio Rapport hauteur/largeur du widget

Décoder co pour retrouver l'origine

co est l'origine encodée en base64. Le point final est un artefact de padding : retirez-le avant de décoder.

import base64

co_value = "aHR0cHM6Ly9leGFtcGxlLmNvbTo0NDM."
# Remove trailing period (padding artifact)
decoded = base64.b64decode(co_value.rstrip(".") + "==").decode()
print(decoded)  # "https://example.com:443"

Vous obtenez https://example.com:443, l'origine exacte pour laquelle le widget est configuré.

Exemple : une équipe QA lyonnaise teste un tunnel de paiement hébergé chez OVHcloud. Le formulaire refuse tous les tokens. Le co décodé donne https://www.exemple.fr quand le script envoie pageurl=https://exemple.fr : une redirection www suffisait. Journalisez l'URL anchor, jamais le contenu des formulaires testés — minimisation RGPD oblige.


Extraire les URL anchor et bframe

Commencez par le HTML statique : si l'anchor y figure, inutile d'ouvrir un navigateur.

En Python, depuis le code source

requests et BeautifulSoup suffisent quand le widget est rendu côté serveur. La fonction lit l'anchor, en tire le sitekey, la version et la langue, décode l'origine et reconstruit la bframe :

import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs
import re
import base64

def extract_recaptcha_iframes(url):
    """Extract reCAPTCHA anchor and bframe iframe URLs and parameters."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) "
                      "Chrome/120.0.0.0 Safari/537.36",
    }

    response = requests.get(url, headers=headers, timeout=15)
    soup = BeautifulSoup(response.text, "html.parser")

    result = {
        "anchor_url": None,
        "bframe_url": None,
        "site_key": None,
        "origin": None,
        "version": None,
        "language": None,
    }

    # Find anchor iframe
    anchor_iframe = soup.find("iframe", src=re.compile(r"recaptcha.*anchor"))
    if anchor_iframe:
        anchor_url = anchor_iframe.get("src", "")
        result["anchor_url"] = anchor_url

        # Parse parameters
        parsed = urlparse(anchor_url)
        params = parse_qs(parsed.query)

        result["site_key"] = params.get("k", [None])[0]
        result["version"] = params.get("v", [None])[0]
        result["language"] = params.get("hl", [None])[0]

        # Decode origin
        co = params.get("co", [None])[0]
        if co:
            try:
                padded = co.rstrip(".") + "=="
                result["origin"] = base64.b64decode(padded).decode()
            except Exception:
                result["origin"] = co

    # Find bframe iframe (may not be in source — loaded dynamically)
    bframe_iframe = soup.find("iframe", src=re.compile(r"recaptcha.*bframe"))
    if bframe_iframe:
        result["bframe_url"] = bframe_iframe.get("src", "")

    # Construct bframe URL from anchor parameters if not found
    if not result["bframe_url"] and result["site_key"] and result["version"]:
        result["bframe_url"] = (
            f"https://www.google.com/recaptcha/api2/bframe?"
            f"hl={result['language'] or 'en'}"
            f"&v={result['version']}"
            f"&k={result['site_key']}"
        )

    return result

iframes = extract_recaptcha_iframes("https://example.com/login")
print(f"Site key: {iframes['site_key']}")
print(f"Origin: {iframes['origin']}")
print(f"Anchor URL: {iframes['anchor_url']}")

En Node.js avec axios et cheerio

Même logique, avec l'objet URL natif pour lire les paramètres :

const axios = require("axios");
const cheerio = require("cheerio");
const { URL } = require("url");

async function extractRecaptchaIframes(pageUrl) {
    const { data: html } = await axios.get(pageUrl, {
        headers: {
            "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
                "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
        },
        timeout: 15000,
    });

    const $ = cheerio.load(html);
    const result = {
        anchorUrl: null,
        bframeUrl: null,
        siteKey: null,
        origin: null,
        version: null,
    };

    // Find anchor iframe
    const anchorIframe = $("iframe[src*='recaptcha'][src*='anchor']");
    if (anchorIframe.length) {
        const src = anchorIframe.attr("src");
        result.anchorUrl = src;

        const url = new URL(src);
        result.siteKey = url.searchParams.get("k");
        result.version = url.searchParams.get("v");

        // Decode origin
        const co = url.searchParams.get("co");
        if (co) {
            try {
                result.origin = Buffer.from(
                    co.replace(/\.$/, ""), "base64"
                ).toString();
            } catch {}
        }
    }

    // Construct bframe URL
    if (result.siteKey && result.version) {
        result.bframeUrl =
            `https://www.google.com/recaptcha/api2/bframe?` +
            `hl=en&v=${result.version}&k=${result.siteKey}`;
    }

    return result;
}

extractRecaptchaIframes("https://example.com/login").then(console.log);

Avec Selenium, quand la page est dynamique

Widget injecté après coup — application monopage, chargement différé, modale — et le HTML initial ne contient rien. Il faut alors un vrai navigateur :

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

def extract_iframes_selenium(url):
    """Extract reCAPTCHA iframe URLs from a dynamically loaded page."""
    driver = webdriver.Chrome()
    driver.get(url)
    time.sleep(3)  # Wait for reCAPTCHA to load

    result = {"anchor_url": None, "bframe_url": None}

    # Find all iframes
    iframes = driver.find_elements(By.TAG_NAME, "iframe")

    for iframe in iframes:
        src = iframe.get_attribute("src") or ""
        if "recaptcha" in src and "anchor" in src:
            result["anchor_url"] = src
        elif "recaptcha" in src and "bframe" in src:
            result["bframe_url"] = src

    driver.quit()
    return result

Pour capturer la bframe, cliquez d'abord sur la case : sans défi, elle n'existe pas.


Quatre cas où l'extraction sert vraiment

La résolution standard n'en demande aucune ; voici les quatre exceptions.

1. Confirmer le bon sitekey

  • Plusieurs widgets : data-sitekey ne dit pas lequel est actif.
  • Clé injectée en JavaScript : l'anchor porte quand même la clé utilisée.
# Extract sitekey from anchor URL when it's not in the page HTML
iframes = extract_recaptcha_iframes(url)
sitekey = iframes["site_key"]  # Reliably present in the iframe URL

2. Distinguer reCAPTCHA v2 et reCAPTCHA v2 Enterprise

  • /api2/anchor : reCAPTCHA v2 classique.
  • /enterprise/anchor : reCAPTCHA v2 Enterprise, pris en charge lui aussi, diagnostic différent.
# The anchor URL reveals the exact reCAPTCHA version
if "/api2/anchor" in anchor_url:
    recaptcha_type = "v2"
elif "/enterprise/anchor" in anchor_url:
    recaptcha_type = "enterprise"

3. Aligner l'origine sur les intégrations strictes de domaine

  • Certaines intégrations comparent l'origine au caractère près.
  • Le pageurl envoyé doit reprendre l'origine décodée, sous-domaine et protocole compris.
# Decode the origin from the co parameter
origin = decode_co_parameter(iframes["co"])
# Use this origin as the pageurl for the solver

4. Comprendre pourquoi un token est refusé

  • L'API renvoie un token, la page le rejette sans message.
  • Comparez les paramètres de l'anchor avec ceux de votre requête.
def debug_solve_params(anchor_url, solver_pageurl, solver_sitekey):
    """Compare anchor params with solver request to find mismatches."""
    parsed = urlparse(anchor_url)
    params = parse_qs(parsed.query)

    issues = []

    # Check sitekey
    anchor_key = params.get("k", [None])[0]
    if anchor_key != solver_sitekey:
        issues.append(f"Sitekey mismatch: anchor={anchor_key}, solver={solver_sitekey}")

    # Check origin
    co = params.get("co", [None])[0]
    if co:
        origin = base64.b64decode(co.rstrip(".") + "==").decode()
        solver_parsed = urlparse(solver_pageurl)
        solver_origin = f"{solver_parsed.scheme}://{solver_parsed.netloc}"
        if origin != solver_origin:
            issues.append(f"Origin mismatch: anchor={origin}, solver={solver_origin}")

    return issues if issues else ["No mismatches found"]

La voie normale : sitekey et pageurl suffisent

Envoyez la clé du site et l'URL de la page à in.php, puis interrogez res.php jusqu'au token g-recaptcha-response :

import requests
import time

API_KEY = "YOUR_API_KEY"

# All you need: sitekey + pageurl
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": "https://example.com/login",
    "json": 1,
})

task_id = submit.json()["request"]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY,
        "action": "get",
        "id": task_id,
        "json": 1,
    }).json()

    if result.get("status") == 1:
        token = result["request"]
        print(f"Token: {token[:50]}...")
        break

Ce script couvre reCAPTCHA v2, sa variante invisible et Enterprise. Le débit dépend de vos threads : BASIC ($15/mois, 5 threads) autorise cinq résolutions simultanées, facturées en dollars US. L'extraction n'intervient qu'ensuite, si cette voie échoue.


Questions fréquentes

Extraire l'URL anchor accélère-t-il la résolution ?

Non. Le temps de résolution dépend du défi servi et des threads disponibles.

CaptchaAI n'utilise que le sitekey et le pageurl : lire l'anchor ne change rien au traitement.

Pourquoi l'iframe bframe est-elle absente du code source ?

Parce qu'elle n'existe pas encore : le JavaScript de reCAPTCHA la crée au clic, quand un défi d'image se déclenche.

Il faut un navigateur piloté — Selenium, Puppeteer — pour la capturer.

Comment récupérer un sitekey chargé en JavaScript ?

Lisez le paramètre k de l'URL anchor plutôt que le HTML.

Il reflète la clé réellement passée au widget, même injectée après coup.

Que faire si le token est refusé alors que le sitekey est bon ?

Décodez co et comparez-le avec le pageurl envoyé.

L'écart tient presque toujours à un www, un port explicite ou une redirection http/https.


À retenir

Pour une résolution classique avec CaptchaAI, sitekey et pageurl suffisent. Gardez l'extraction pour les cas ambigus :

  • Deux iframes : anchor pour le widget, bframe pour la grille d'images.
  • L'URL anchor donne le sitekey réellement chargé, l'origine encodée et le hash de version.
  • Débogage de domaine et pages dynamiques : l'extraction paie là.

Articles connexes

Les commentaires sont désactivés pour cet article.