Tutorials

Извлечение CAPTCHA из iframe: решение CAPTCHA во вложенных фреймах

Скрипт нашёл .g-recaptcha, отправил sitekey в API, получил токен — а сайт всё равно отклоняет запрос. Частая причина: виджет CAPTCHA рендерится не на верхнем уровне страницы, а внутри вложенного iframe (например, платёжного iframe внутри модального окна), и ключ сайта взят не из того фрейма.

Коротко: найдите iframe, который рендерит .g-recaptcha, возьмите sitekey из его data-sitekey (или k= в src), решите через CaptchaAI и верните токен в тот же фрейм — не в родительскую страницу и не во внутренний Google-фрейм визуальной задачи.

Разбираемся, как пройти по дереву iframe, достать sitekey с нужного уровня, решить CAPTCHA через CaptchaAI и вернуть токен точно в тот фрейм, где его ждёт форма.


Структура вложенных iframe на практике

Типичная вложенная структура:

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

Ключ сайта лежит во внешнем iframe — том, который непосредственно рендерит виджет. Самый внутренний Google-фрейм отвечает только за визуальную головоломку, работать с ним напрямую не нужно. Нужное значение — атрибут data-sitekey внешнего фрейма или параметр k= в его src. Именно эту путаницу уровней чаще всего описывают в тикетах: sitekey берут из самого глубокого фрейма (Google-виджета) или с родительской страницы — и получают ERROR_WRONG_GOOGLEKEY.

Как быстро понять, что sitekey нужно искать во вложенном iframe, а не на верхнем уровне страницы:

  • document.querySelector('.g-recaptcha') на главном документе возвращает null;
  • в DevTools виджет CAPTCHA лежит в отдельном узле #document внутри <iframe>, а не в основном DOM-дереве страницы;
  • попытка прочитать data-sitekey с элемента, найденного на верхнем уровне, возвращает undefined.

Python: переключение между iframe в Selenium

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

Ожидаемый результат:

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

JavaScript: работа с фреймами в Puppeteer

В отличие от Selenium, в Puppeteer не нужно явно «переключаться» между фреймами — у ElementHandle iframe есть метод contentFrame() с прямым доступом к вложенному документу.

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');
})();

Типичные ошибки при работе с вложенными CAPTCHA

Большинство тикетов в поддержку по вложенным CAPTCHA сводятся к одной из четырёх причин:

  • В pageurl передан адрес главной страницы, а не фрейма — сайт отклоняет токен. Передавайте URL фрейма, где рендерится виджет CAPTCHA.
  • Скрипт не вернулся в родительский фрейм после инъекции токена — дальнейшие клики «не находят» элементы. Вызывайте switch_to.parent_frame() (Selenium) или работайте через объект родительского фрейма (Puppeteer).
  • sitekey взят из внутреннего Google-фрейма, а не из внешнего — API возвращает ERROR_WRONG_GOOGLEKEY. Берите ключ из iframe с .g-recaptcha, а не из вложенного фрейма визуальной задачи.
  • Попытка выполнить execute_script внутри cross-origin iframeSecurityError в консоли. JS в чужом источнике не выполняется — читайте sitekey из атрибута src.

Похожая структура встречается и за пределами reCAPTCHA — например, на визовых порталах вроде BLS, где CAPTCHA формы записи тоже вложена в iframe. Диагностика та же: сначала найдите фрейм, который рендерит виджет, потом берите sitekey.

Перед тем как выкатывать такой скрипт в CI, проверьте:

  • pageurl соответствует фрейму, где реально рендерится виджет, а не корневой странице;
  • после инъекции токена скрипт вернулся в родительский фрейм (switch_to.parent_frame() или объект родительского фрейма в Puppeteer);
  • sitekey не меняется между прогонами — если сайт ротирует ключ, логируйте его перед каждой отправкой в API, а не берите из закешированного значения.

В CI на staging логируйте src найденных iframe — экономит время при редизайне у клиента. Храните только то, что нужно для отладки, с оглядкой на 152-ФЗ.


Если уровней вложенности три и больше

Схема та же, просто цепочка длиннее: переключаетесь на первый iframe, из него — на второй, и так до фрейма с CAPTCHA. Возврат — тоже по цепочке, уровень за уровнем.

Такая многоуровневая вложенность встречается не только у reCAPTCHA — сторонние платёжные виджеты и партнёрские формы записи (визовые порталы, бронирование) нередко оборачивают CAPTCHA в два-три служебных iframe: аналитика, антифрод-скрипт и сам виджет каждый в своём фрейме. Логика поиска не меняется на каждом уровне:

  1. найдите iframe текущего уровня и переключитесь внутрь;
  2. проверьте, есть ли внутри ещё один iframe с виджетом — если да, повторите шаг 1;
  3. на последнем уровне читайте sitekey и работайте с CAPTCHA как обычно.

Пропускать уровни через switch_to.frame() нельзя — только последовательно, один шаг за раз.

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

Вопросы по решению CAPTCHA во вложенных iframe

Как понять, что sitekey нужно брать именно из iframe, а не с родительской страницы?

Если .g-recaptcha не находится прямым поиском по DOM верхнего уровня — виджет внутри iframe. Два способа проверить это за минуту:

  • разверните дерево фреймов в DevTools (вкладка Elements → переключатель фреймов в верхней панели) и найдите документ, где лежит .g-recaptcha;
  • если документ найден не в главном фрейме — ключ привязан именно к нему, а не к странице, которая его подключила.

Почему API возвращает ERROR_WRONG_GOOGLEKEY, хотя sitekey скопирован из iframe?

Чаще всего ключ взят с внутреннего Google-фрейма (recaptcha/api2/anchor), а не с внешнего iframe сайта. У этих фреймов разный src, и параметр k= для API нужен именно из внешнего.

Нужно ли дожидаться полной загрузки iframe, прежде чем читать sitekey?

Да, обязательно. Для асинхронно подгружаемых iframe (платёжные и модальные окна):

  • в Selenium — явный WebDriverWait на сам iframe перед switch_to.frame();
  • в Puppeteer — waitForSelector на iframe перед вызовом contentFrame().

Без ожидания скрипт прочитает src до отрисовки элемента и получит None/null вместо реального значения.

В какой фрейм вставлять токен, если вложенность — три уровня и больше?

В тот, где находится textarea[name="g-recaptcha-response"] — фрейм, отрендеривший .g-recaptcha, сколько бы уровней iframe ни было выше. Промежуточные фреймы токен не трогают, они нужны только для навигации.

У iframe вообще нет атрибута src — виджет вставлен через document.write или srcdoc. Что делать?

Тогда k= из URL не достать. Дождитесь события load на самом iframe и читайте data-sitekey напрямую с div.g-recaptcha внутри его документа — через contentDocument, если фрейм same-origin, или через postMessage, если скрипт виджета его отправляет. Для cross-origin без postMessage остаётся только атрибут src, если он появляется динамически после рендера — ждите его через MutationObserver, а не читайте один раз при первой загрузке.


Решайте CAPTCHA на любой глубине iframe с CaptchaAI

Получите API-ключ на captchaai.com.


Связанные руководства

Комментарии для этой статьи отключены.