Tutorial

CAPTCHA di Shadow DOM: Mengakses Elemen di Web Components

document.querySelector standard tidak dapat menjangkau bagian dalam akar bayangan. Saat widget CAPTCHA dirender di dalam shadow DOM komponen web, skrip ekstraksi sitekey Anda yang biasa akan mengembalikan null. CAPTCHA ada di sana – pemilih Anda tidak dapat melihatnya.

Mengapa CAPTCHA Berakhir di Shadow DOM

Skenario Alasan
Komponen login khusus Dienkapsulasi sebagai komponen web yang dapat digunakan kembali
Widget formulir pihak ketiga Vendor widget membungkus seluruh formulir dalam shadow root
Arsitektur mikro-frontend Setiap aplikasi mikro menggunakan DOM bayangan terisolasi
Komponen sistem desain CAPTCHA tertanam dalam elemen perpustakaan komponen

Mendeteksi CAPTCHA Shadow DOM

Sebelum menyelesaikan, pastikan CAPTCHA ada di dalam shadow root:

// In browser DevTools console
// Regular query returns null even though CAPTCHA is visible
document.querySelector('.cf-turnstile');  // null

// Check for shadow hosts
document.querySelectorAll('*').forEach(el => {
  if (el.shadowRoot) {
    const captcha = el.shadowRoot.querySelector('.cf-turnstile, .g-recaptcha');
    if (captcha) {
      console.log('Found CAPTCHA in shadow root of:', el.tagName, el.id || el.className);
      console.log('Sitekey:', captcha.dataset.sitekey);
    }
  }
});

Python: Penulis Drama Shadow DOM Piercing

Pemilih penusuk >> penulis drama dan API locator menangani shadow DOM secara asli:

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_turnstile(sitekey, pageurl):
    """Submit and poll a Turnstile CAPTCHA."""
    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": pageurl,
        "json": 1,
    }, 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 extract_from_shadow_dom(page):
    """Extract CAPTCHA sitekey from shadow DOM elements."""

    # Method 1: Playwright's piercing selector (>>)
    # This automatically crosses shadow boundaries
    turnstile = page.locator("css=.cf-turnstile >> visible=true").first
    if turnstile.count() > 0:
        sitekey = turnstile.get_attribute("data-sitekey")
        if sitekey:
            return sitekey

    # Method 2: JavaScript evaluation to pierce all shadow roots
    sitekey = page.evaluate("""
        () => {
            function findInShadowRoots(root) {
                // Check direct children
                const turnstile = root.querySelector('.cf-turnstile');
                if (turnstile && turnstile.dataset.sitekey) {
                    return turnstile.dataset.sitekey;
                }

                const recaptcha = root.querySelector('.g-recaptcha');
                if (recaptcha && recaptcha.dataset.sitekey) {
                    return recaptcha.dataset.sitekey;
                }

                // Recurse into nested shadow roots
                for (const el of root.querySelectorAll('*')) {
                    if (el.shadowRoot) {
                        const found = findInShadowRoots(el.shadowRoot);
                        if (found) return found;
                    }
                }
                return null;
            }
            return findInShadowRoots(document);
        }
    """)

    return sitekey


def inject_token_shadow_dom(page, token, captcha_type="turnstile"):
    """Inject solved token into shadow DOM CAPTCHA element."""
    if captcha_type == "turnstile":
        page.evaluate(f"""
            (token) => {{
                function findAndInject(root) {{
                    // Find the response input inside Turnstile
                    const input = root.querySelector('[name="cf-turnstile-response"]');
                    if (input) {{
                        input.value = token;
                        return true;
                    }}

                    // Recurse into shadow roots
                    for (const el of root.querySelectorAll('*')) {{
                        if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
                            return true;
                        }}
                    }}
                    return false;
                }}
                findAndInject(document);

                // Also try callback if defined
                if (typeof window.turnstileCallback === 'function') {{
                    window.turnstileCallback(token);
                }}
            }}
        """, token)
    elif captcha_type == "recaptcha":
        page.evaluate(f"""
            (token) => {{
                function findAndInject(root) {{
                    const textarea = root.querySelector('#g-recaptcha-response');
                    if (textarea) {{
                        textarea.value = token;
                        textarea.style.display = 'block';
                        return true;
                    }}
                    for (const el of root.querySelectorAll('*')) {{
                        if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
                            return true;
                        }}
                    }}
                    return false;
                }}
                findAndInject(document);

                if (typeof ___grecaptcha_cfg !== 'undefined') {{
                    Object.entries(___grecaptcha_cfg.clients).forEach(([_, client]) => {{
                        Object.entries(client).forEach(([_, val]) => {{
                            if (val && typeof val === 'object') {{
                                Object.entries(val).forEach(([_, v]) => {{
                                    if (v && v.callback) v.callback(token);
                                }});
                            }}
                        }});
                    }});
                }}
            }}
        """, token)


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

        # Extract sitekey from shadow DOM
        sitekey = extract_from_shadow_dom(page)
        if not sitekey:
            print("No CAPTCHA found in shadow DOM or regular DOM")
            browser.close()
            return

        print(f"Found sitekey: {sitekey}")

        # Solve via CaptchaAI
        token = solve_turnstile(sitekey, page.url)
        print(f"Solved: {token[:40]}...")

        # Inject token back into shadow DOM
        inject_token_shadow_dom(page, token, "turnstile")
        print("Token injected into shadow DOM")

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

        browser.close()


main()

JavaScript: Traversal DOM Bayangan Dalang

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 solveTurnstile(sitekey, pageurl) {
  const params = new URLSearchParams({
    key: API_KEY, method: "turnstile", sitekey, pageurl, json: "1",
  });
  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body: params })).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 extractSitekeyFromShadowDOM(page) {
  return page.evaluate(() => {
    function searchShadowRoots(root) {
      const selectors = [".cf-turnstile", ".g-recaptcha", ".h-captcha"];
      for (const sel of selectors) {
        const el = root.querySelector(sel);
        if (el && el.dataset.sitekey) return el.dataset.sitekey;
      }
      for (const el of root.querySelectorAll("*")) {
        if (el.shadowRoot) {
          const found = searchShadowRoots(el.shadowRoot);
          if (found) return found;
        }
      }
      return null;
    }
    return searchShadowRoots(document);
  });
}

async function injectTokenShadowDOM(page, token) {
  await page.evaluate((t) => {
    function inject(root) {
      const input = root.querySelector('[name="cf-turnstile-response"]');
      if (input) { input.value = t; return true; }
      const textarea = root.querySelector("#g-recaptcha-response");
      if (textarea) { textarea.value = t; return true; }
      for (const el of root.querySelectorAll("*")) {
        if (el.shadowRoot && inject(el.shadowRoot)) return true;
      }
      return false;
    }
    inject(document);
  }, token);
}

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

  const sitekey = await extractSitekeyFromShadowDOM(page);
  if (!sitekey) {
    console.log("No CAPTCHA found in shadow DOM");
    await browser.close();
    return;
  }

  console.log(`Sitekey: ${sitekey}`);
  const token = await solveTurnstile(sitekey, page.url());
  console.log(`Solved: ${token.substring(0, 40)}...`);

  await injectTokenShadowDOM(page, token);
  await page.click('button[type="submit"]');
  await page.waitForNavigation();

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

Pertimbangan Kedalaman Shadow DOM

Kedalaman Contoh Pendekatan
1 tingkat <custom-form> #shadow-root > .cf-turnstile Cookieri root bayangan langsung
2+ tingkat <app-shell> #shadow > <login-form> #shadow > .cf-turnstile Penjelajahan rekursif
Buka akar bayangan el.shadowRoot dapat diakses Pendekatan standard – gunakan rekursi
Akar bayangan tertutup el.shadowRoot mengembalikan nol Tidak bisa menembus; gunakan page.evaluate dengan attachShadow({mode:'open'}) menimpa atau mencegat rendering

Urutan diagnostik cadangan

  • Periksa akar bayangan terbuka terlebih dahulu, lalu batas iframe, dan baru kemudian callback tingkat aplikasi atau hook yang disuntikkan.
  • Catat lapisan mana yang terkena tantangan sehingga perubahan target di masa depan lebih mudah dikenali.
  • Pertahankan pilihan terakhir untuk root tertutup daripada mengasumsikan setiap halaman memperlihatkan akses DOM secara seragam.

Urutan diagnostik cadangan

  • Periksa akar bayangan terbuka terlebih dahulu, lalu batas iframe, dan baru kemudian callback tingkat aplikasi atau hook yang disuntikkan.
  • Catat lapisan mana yang terkena tantangan sehingga perubahan target di masa depan lebih mudah dikenali.
  • Pertahankan pilihan terakhir untuk root tertutup daripada mengasumsikan setiap halaman memperlihatkan akses DOM secara seragam.

Pemecahan Masalah

Masalah Penyebab Solusi
shadowRoot mengembalikan null DOM bayangan tertutup Ganti attachShadow sebelum halaman dimuat untuk memaksa mode: 'open'
Sitekey ditemukan tetapi token ditolak Pageurl salah dikirim ke pemecah Gunakan page.url() pada waktu ekstraksi, bukan URL hardcode
Token disuntikkan tetapi formulir tidak dikirimkan Callback CAPTCHA tidak terpicu Temukan dan panggil fungsi callback setelah mengatur nilai token
Pencarian rekursif lambat Pohon DOM yang dalam Batasi kedalaman rekursi; menargetkan nama tag host bayangan yang dikenal
CAPTCHA tidak ditemukan pada pemuatan awal Widget dimuat secara asinkron Tunggu elemen shadow host terlebih dahulu: page.waitForSelector('custom-form')

Pertanyaan Umum

Bagaimana saya tahu jika CAPTCHA menggunakan shadow DOM?

Di Chrome DevTools, periksa elemen CAPTCHA. Jika Anda melihat #shadow-root (open) atau #shadow-root (closed) di atas markup CAPTCHA, itu berarti berada di dalam shadow root. querySelector reguler dari document tidak akan menemukannya.

Bisakah saya menghindari traversal shadow DOM sepenuhnya?

Jika Anda menggunakan CaptchaAI API secara langsung (bukan injeksi browser), Anda hanya memerlukan sitekey dan pageurl - shadow DOM tidak masalah. Penanganan Shadow DOM hanya diperlukan ketika Anda harus mengekstrak sitekey dari halaman aktif atau memasukkan token kembali ke DOM.

Bagaimana dengan akar bayangan tertutup?

Akar bayangan tertutup memblokir akses el.shadowRoot. Solusinya adalah dengan mengganti Element.prototype.attachShadow sebelum halaman dimuat, memaksa semua akar bayangan ke mode terbuka. Ini berfungsi di Penulis Drama dan Dalang melalui page.evaluateOnNewDocument.

Artikel Terkait

Langkah Selanjutnya

Menangani CAPTCHA yang tersembunyi di dalam komponen web —dapatkan kunci API CaptchaAI Andadan menerapkan traversal shadow DOM.

Panduan terkait:

  • Menangani Beberapa CAPTCHA dalam Satu Halaman
  • Ekstraksi CAPTCHA Iframe: Bingkai Bersarang
  • CAPTCHA dalam Modal Pop-Up: Deteksi dan Injeksi
Komentar dinonaktifkan untuk artikel ini.