Tutorials

Extração de Iframe CAPTCHA: Resolvendo CAPTCHAs em quadros aninhados

Você troca para o iframe certo, chama execute_script e recebe SecurityError. Ou o token é aceito pela CaptchaAI mas rejeitado pelo formulário, porque a pageurl era a da página-mãe, não a do frame que carrega o widget. Em checkout com CAPTCHA embutido, o widget quase nunca está solto — mora num iframe de pagamento, dentro de um iframe modal. Resolver o desafio é fácil; chegar à sitekey certa, no frame certo, é onde a integração trava.

Este guia mostra a árvore de iframes, a extração da sitekey sem tocar em JS de origem cruzada e a injeção do token no frame exato — com Selenium e Puppeteer.


A árvore de iframes: onde a sitekey realmente está

Uma estrutura aninhada típica em checkout:

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

A sitekey não está no iframe mais interno — está no iframe que renderiza o widget, um nível acima do desafio visual do Google, que só desenha o captcha. Você precisa do data-sitekey ou do k= na URL (src) do iframe do meio; confundir os dois é a causa mais comum de ERROR_WRONG_GOOGLEKEY. Mesmo de outra origem, ler o src a partir do frame pai não viola CORS — isso só importa se você rodar JS dentro do iframe.

Três sinais de que você está olhando o frame errado antes mesmo de rodar código:

  • O src do iframe não contém recaptcha/api2/anchor nem challenges.cloudflare.com — você ainda está um nível acima do widget.
  • O elemento tem .g-recaptcha ou .cf-turnstile como classe, mas nenhum atributo data-sitekey visível — a sitekey pode estar só na URL (k=), não no atributo.
  • driver.find_element ou page.$ não encontra nada mesmo com o seletor certo — você provavelmente não trocou de contexto e ainda está no documento principal.

Selenium em Python: trocando de frame para extrair a sitekey

Com Selenium é preciso trocar de contexto antes de localizar qualquer elemento dentro do iframe — não há atalho.

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 with 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")

Saída esperada:

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

Repare no passo 5: sem switch_to.default_content(), o clique seguinte falha silenciosamente, porque o Selenium continua "preso" dentro do iframe de pagamento.


Puppeteer em Node.js: sem troca manual de frame

O Puppeteer trata cada iframe como um Frame — você navega até ele com contentFrame(), sem "trocar" de contexto como no 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 with 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');
})();

A vantagem: paymentFrame é um objeto que você passa adiante, sem risco de "esquecer" o frame ativo.


Três ou mais níveis de aninhamento

Com o CAPTCHA a três ou mais camadas — comum em modais de terceiros — encadeie as trocas de frame uma a uma.

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

A regra é a mesma: você só enxerga elementos do frame em que está — não dá para pular direto ao frame 3.


Cloudflare Turnstile aninhado: o que muda na prática

Diferente do reCAPTCHA, o Turnstile costuma expor a data-sitekey direto no elemento .cf-turnstile da página host — muitas integrações de checkout nem colocam o widget dentro de um segundo iframe cross-origin. Quando o gateway de pagamento o embute num modal, porém, o fluxo de troca de frame é idêntico ao descrito acima para o reCAPTCHA.

Checklist rápido para QA que testa os dois solvers no mesmo fluxo de checkout:

  • Leia a sitekey do atributo data-sitekey no .cf-turnstile, não da URL de um iframe interno do Cloudflare.
  • Injete o token no campo cf-turnstile-response, nunca em g-recaptcha-response — os dois convivem no mesmo formulário em integrações híbridas.
  • O Turnstile normalmente resolve em menos de 10 segundos; se o seu loop de polling (24 tentativas de 5s) estourar o timeout, o problema quase sempre é o frame errado, não o solver.

Erros comuns na extração de CAPTCHA em iframe

URL da página principal usada como pageurl

O token é aceito pelo res.php mas rejeitado pelo formulário, porque o servidor valida a URL do documento que efetivamente carregou o widget — não a da página-mãe. Corrija enviando a URL do iframe que renderiza o .g-recaptcha (ou .cf-turnstile).

Não voltar ao frame pai depois de injetar o token

Cliques e envios seguintes falham silenciosamente, porque o driver continua "preso" dentro do iframe onde a injeção aconteceu. Chame switch_to.parent_frame() ou switch_to.default_content() (Selenium) logo após a injeção; no Puppeteer, volte a operar sobre page em vez do objeto Frame.

Sitekey extraída do iframe errado

Resulta em ERROR_WRONG_GOOGLEKEY na resposta do in.php. A sitekey correta está sempre no iframe que contém .g-recaptcha, nunca no desafio visual interno do Google — confundir os dois níveis é o erro mais comum de quem está automatizando esse fluxo pela primeira vez.

JS bloqueado por origem cruzada

Aparece como SecurityError no console do navegador ao tentar execute_script/evaluate dentro do iframe de outra origem. Extraia a sitekey lendo o atributo src a partir do frame pai — isso não exige acesso ao DOM interno do iframe nem viola a política de mesma origem.


Perguntas frequentes

O waitForSelector encontra o iframe de pagamento, mas não o do reCAPTCHA lá dentro. Por quê?

Timing. Muitos gateways carregam o iframe de pagamento primeiro e só injetam o reCAPTCHA depois, via JS assíncrono — algo que times de QA testando checkout em staging a partir de sa-east-1 (São Paulo) sentem mais, pela latência extra. Espere pelo seletor dentro do frame de pagamento e aumente o timeout se o gateway for lento.

Por que meu token é aceito no res.php mas rejeitado pelo formulário?

Quase sempre a pageurl errada: envie a URL do documento que carrega o widget.

A sitekey muda a cada carregamento, ou posso guardá-la em cache?

Ela é fixa por site, então dá para fazer cache em QA. O que muda é o pageurl (com parâmetros dinâmicos) e o token, sempre de uso único.

Isso funciona igual para Cloudflare Turnstile em iframe aninhado?

O princípio é o mesmo: trocar de frame, extrair a sitekey (data-sitekey no .cf-turnstile) e injetar em cf-turnstile-response. O Turnstile costuma expor a sitekey de forma mais direta — muitas vezes nem é preciso entrar no iframe.


Resolva CAPTCHAs em qualquer profundidade de iframe com CaptchaAI

Obtenha sua chave API em captchaai.com.


Guias relacionados

Os comentários estão desativados para este artigo.