Tutoriales

Extracción de CAPTCHA de iframe: resolución de CAPTCHA en marcos anidados

¿Buscas el sitekey en el HTML de la página y no aparece por ningún lado, o la API te devuelve ERROR_WRONG_GOOGLEKEY? En la gran mayoría de los casos la causa es la misma: el widget de reCAPTCHA no está en la página principal, sino dentro de uno o varios iframes anidados. El sitekey vive en ese marco interno y el token resuelto hay que devolverlo al contexto exacto donde se renderiza el widget, no a la página de nivel superior.

La solución siempre sigue los mismos cuatro pasos:

  1. Recorrer el árbol de iframes hasta el marco que renderiza el widget.
  2. Extraer el sitekey de ese marco.
  3. Resolver el desafío con CaptchaAI.
  4. Inyectar el token en el textarea correcto y disparar el callback.

En esta guía lo verás con Selenium (Python) y Puppeteer (JavaScript), incluidos los iframes de origen cruzado y con tres o más niveles de anidamiento.


Por qué el CAPTCHA vive dentro de un iframe

Muchos sitios no colocan el reCAPTCHA directamente en su página: lo cargan a través de un componente de terceros que se inyecta en un iframe. Es lo que ocurre en escenarios muy comunes para el desarrollador hispanohablante:

  • Pasarelas de pago incrustadas en el checkout de una tienda online.
  • Ventanas modales de registro o de inicio de sesión.
  • Formularios de portales de cita previa y trámites de administración pública.

El resultado es un widget que se renderiza dos o tres niveles por debajo del documento principal, y ahí es donde empiezan los problemas de extracción. Una estructura anidada típica se ve así:

Main page
  └── iframe#payment-frame (cross-origin)
        └── iframe[src*="recaptcha/api2/anchor"] (Google-hosted)
              └── reCAPTCHA widget

En esta estructura conviene tener claro dónde está cada pieza:

  • El sitekey vive en el iframe exterior, el que representa el widget.
  • El iframe más interno alojado por Google es solo el desafío visual: no interactúas con él directamente.
  • Lo que necesitas es el data-sitekey del elemento .g-recaptcha o, si no está expuesto, el parámetro k= dentro del src del iframe. Ambos son legibles desde el marco padre.

Errores comunes al resolver CAPTCHA en iframes

Antes de escribir código, conviene tener presentes los fallos que rompen la extracción. Estos cuatro concentran la mayoría de los tickets:

Error Qué ocurre Solución
Usar la URL de la página principal como pageurl El sitio rechaza el token Usa la URL del marco que renderiza el widget CAPTCHA
No volver al marco padre tras inyectar Las acciones siguientes fallan Llama a switch_to.parent_frame() o switch_to.default_content() después de la inyección
Leer el sitekey del iframe equivocado ERROR_WRONG_GOOGLEKEY El sitekey está en el iframe que contiene .g-recaptcha, no en el iframe interno del desafío de Google
Ejecutar JS dentro de un iframe cross-origin SecurityError en la consola No puedes usar execute_script dentro de un iframe de origen cruzado; extrae el sitekey del atributo src

Con el mapa de errores claro, veamos la implementación correcta.


Extraer el sitekey con Selenium (Python)

Selenium exige cambiar de contexto manualmente: switch_to.frame() para entrar en cada iframe y switch_to.default_content() para volver a la raíz. El flujo completo hace lo siguiente:

  • Entra en el iframe de pago exterior.
  • Localiza el iframe de reCAPTCHA y lee el sitekey de su atributo src.
  • Resuelve el desafío con CaptchaAI y espera el token.
  • Inyecta el token sin salir del marco y vuelve al contexto principal.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import requests
import re
import time

API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example.com/checkout")

# Step 1: Switch into the outer iframe
wait = WebDriverWait(driver, 15)
outer_iframe = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "iframe#payment-frame"))
)
driver.switch_to.frame(outer_iframe)

# Step 2: Find the reCAPTCHA iframe and extract sitekey
recaptcha_iframe = wait.until(
    EC.presence_of_element_located(
        (By.CSS_SELECTOR, 'iframe[src*="recaptcha/api2/anchor"]')
    )
)
src = recaptcha_iframe.get_attribute("src")
sitekey = re.search(r"k=([A-Za-z0-9_-]+)", src).group(1)
page_url = driver.current_url
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")

# Step 3: Solve con CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": page_url,
    "json": "1"
})
task_id = resp.json()["request"]

token = None
for _ in range(24):
    time.sleep(5)
    poll = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1"
    }).json()
    if poll["status"] == 1:
        token = poll["request"]
        break
    if poll["request"] != "CAPCHA_NOT_READY":
        raise Exception(poll["request"])

print(f"Token: {token[:50]}...")

# Step 4: Inject token (still inside the outer iframe)
driver.execute_script("""
    document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
    if (typeof grecaptcha !== 'undefined') {
        grecaptcha.getResponse = function() { return arguments[0]; };
    }
""", token)

# Step 5: Switch back to main page
driver.switch_to.default_content()
print("Token injected, switched back to main frame")

Resultado esperado:

Sitekey: 6Le-SITEKEY-abc123
Page URL: https://example.com/checkout
Token: 03AGdBq26ZfPxL...
Token injected, switched back to main frame

Manejo de marcos con Puppeteer (JavaScript)

Puppeteer trata cada iframe como un objeto Frame de primera clase, así que no hace falta cambiar de contexto manualmente: obtienes un handle del iframe con contentFrame() y trabajas dentro de él directamente. Esto simplifica mucho el anidamiento respecto a Selenium.

const puppeteer = require('puppeteer');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

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

  // Step 1: Find the payment iframe
  const paymentFrameHandle = await page.waitForSelector('iframe#payment-frame');
  const paymentFrame = await paymentFrameHandle.contentFrame();

  // Step 2: Find reCAPTCHA iframe inside the payment frame
  const recaptchaFrameHandle = await paymentFrame.waitForSelector(
    'iframe[src*="recaptcha/api2/anchor"]'
  );
  const src = await paymentFrame.evaluate(
    el => el.getAttribute('src'), recaptchaFrameHandle
  );

  const sitekey = src.match(/k=([A-Za-z0-9_-]+)/)[1];
  const pageUrl = page.url();
  console.log(`Sitekey: ${sitekey}`);

  // Step 3: Solve con CaptchaAI
  const submitResp = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: {
      key: API_KEY,
      method: 'userrecaptcha',
      googlekey: sitekey,
      pageurl: pageUrl,
      json: 1
    }
  });
  const taskId = submitResp.data.request;

  let token = null;
  for (let i = 0; i < 24; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const pollResp = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
    });
    if (pollResp.data.status === 1) {
      token = pollResp.data.request;
      break;
    }
    if (pollResp.data.request !== 'CAPCHA_NOT_READY') {
      throw new Error(pollResp.data.request);
    }
  }

  console.log(`Token: ${token.substring(0, 50)}...`);

  // Step 4: Inject token into the payment frame
  await paymentFrame.evaluate((tkn) => {
    const textarea = document.querySelector(
      'textarea[name="g-recaptcha-response"]'
    );
    textarea.value = tkn;

    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && typeof window[callback] === 'function') {
      window[callback](tkn);
    }
  }, token);

  console.log('Token injected into payment iframe');
})();

Cómo localizar el marco correcto

Cuando una página tiene varios iframes, conviene recorrerlos antes de resolver nada:

  • Selenium: lista todos los marcos con driver.find_elements(By.TAG_NAME, "iframe") y revisa el src de cada uno; el que contiene recaptcha/api2/anchor es el del widget.
  • Puppeteer: page.frames() te devuelve el árbol completo y puedes filtrar por frame.url().

La regla práctica: el marco que buscas es el que contiene el div con la clase .g-recaptcha, no el iframe interno del desafío de Google.


iframes con anidamiento profundo (tres niveles o más)

Cuando el widget está a tres o más niveles de profundidad, encadena los cambios de marco uno tras otro. La lógica es la misma que con dos niveles, solo que repites el descenso hasta llegar al contexto donde vive el sitekey.

Selenium

# Main → iframe-1 → iframe-2 → CAPTCHA
driver.switch_to.frame(driver.find_element(By.ID, "iframe-1"))
driver.switch_to.frame(driver.find_element(By.ID, "iframe-2"))
# Now extract sitekey from this context

# To go back up one level:
driver.switch_to.parent_frame()

# To go back to main:
driver.switch_to.default_content()

Puppeteer

const frame1 = await (await page.$('iframe#iframe-1')).contentFrame();
const frame2 = await (await frame1.$('iframe#iframe-2')).contentFrame();
// Extract sitekey from frame2

Cuando un marco no carga a tiempo

Un consejo para depurar: si un switch_to.frame() lanza NoSuchFrameException o un waitForSelector se agota, casi siempre es porque el iframe aún no ha terminado de cargar. Espera a que el marco esté presente antes de entrar en él, en lugar de asumir que ya existe.


Preguntas frecuentes

¿Cómo encuentro el iframe correcto cuando la página tiene muchos?

Enumera todos los marcos y filtra por su src. En Selenium usa driver.find_elements(By.TAG_NAME, "iframe") y en Puppeteer page.frames(); el marco del widget es el que contiene recaptcha/api2/anchor en la URL, o el que aloja el div con la clase .g-recaptcha. Descarta el iframe interno de Google, que solo dibuja el desafío.

¿El mismo enfoque sirve para un Cloudflare Turnstile incrustado en un iframe?

Sí. El patrón es idéntico: localiza el marco, lee el sitekey del atributo data-sitekey, resuélvelo con CaptchaAI e inyecta el token de vuelta en el contexto correcto. La única diferencia es el nombre del campo donde se inyecta el token, que cambia según el tipo de CAPTCHA.

¿Por qué falla el envío del formulario aunque el token sea válido?

Depende: casi siempre falta disparar el callback del widget. Muchos sitios no leen el textarea directamente, sino que esperan la función indicada en data-callback. Tras escribir el token, invoca ese callback (como se ve en el ejemplo de Puppeteer) para que la página lo dé por resuelto.

¿Necesito un navegador para extraer el sitekey?

No siempre. Si el sitekey aparece en el HTML inicial que devuelve una petición HTTP, puedes leerlo con una biblioteca de parseo sin abrir un navegador. Pero cuando el widget se carga por JavaScript dentro de un iframe anidado, un navegador controlado con Selenium o Puppeteer es la vía fiable, porque necesitas que los marcos se rendericen antes de leerlos.


Resuelve el CAPTCHA a cualquier profundidad de iframe con CaptchaAI

Ya sea un reCAPTCHA en una pasarela de pago o un Turnstile dentro de una ventana modal, el flujo es el mismo: extrae el sitekey, resuelve con la API e inyecta el token en el marco correcto. Con CaptchaAI trabajas sobre planes basados en threads con resoluciones ilimitadas por thread, desde el plan BASIC ($15/mes, 5 threads), así que puedes escalar tu volumen sin pagar por resolución. Consigue tu clave API en captchaai.com.


Guías relacionadas

Los comentarios están deshabilitados para este artículo.