Tutorials

Shadow DOM CAPTCHA İşleme: Web Bileşenlerindeki Öğelere Ulaşma

Bir CAPTCHA'yı çözmek istiyorsanız önce onu bulmanız gerekir — Shadow DOM'da işin zorlaştığı nokta tam da budur. Bileşen tabanlı arayüzler (Lit, Stencil, Angular Elements ya da tasarım sistemi kütüphaneleri) Turnstile veya reCAPTCHA widget'ını yeniden kullanılabilir bir web bileşeninin gölge köküne gömer; bu kök, standart DOM sorgularının erişemediği yalıtılmış bir katmandır.

İyi haber şu: bu bir engel değil, yalnızca fazladan bir adımdır. Python + Playwright ve Node.js + Puppeteer örnekleriyle akış üç net adıma iner:

  • Gölge kökü delip sitekey'i canlı sayfadan çıkarın.
  • CaptchaAI ile Turnstile ya da reCAPTCHA token'ını çözün.
  • Çözülmüş token'ı web bileşeninin içine geri enjekte edin.

Shadow DOM içindeki CAPTCHA'yı nasıl tespit edersiniz?

Çözüme başlamadan önce CAPTCHA'nın gerçekten bir gölge kökün içinde olduğunu doğrulayın. document üzerinden çalıştırılan normal bir querySelector null döndürüyor ama widget ekranda görünüyorsa, güçlü bir işaret var demektir. Aşağıdaki konsol kod parçası tüm gölge ana makineleri tarar ve bulduğu sitekey'i ekrana basar:

// 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);
    }
  }
});

CAPTCHA neden Shadow DOM'a düşer?

Gölge köke düşmenin birkaç tipik nedeni var; hepsi kapsülleme kararıyla ilgilidir:

  • Özel giriş bileşeni: yeniden kullanılabilir bir web bileşeni olarak kapsüllenir.
  • Üçüncü taraf form widget'ı: sağlayıcı tüm formu kendi gölge köküne sarar.
  • Mikro ön uç (micro-frontend) mimarisi: her mikro uygulama kendi yalıtılmış Shadow DOM'unu kullanır.
  • Tasarım sistemi bileşenleri: CAPTCHA, bileşen kütüphanesindeki bir öğeye gömülüdür.

Yerel senaryo: bileşen tabanlı bir ödeme akışının QA'i

Türkiye'deki e-ticaret ve fintech ekiplerinin çoğu giriş ve ödeme formlarını yeniden kullanılabilir web bileşenleri olarak paketler; aynı <checkout-form> bileşeni birden fazla markanın sitesinde çalışır. Bu bileşenin QA testini otomatikleştirdiğinizde, Turnstile widget'ı bileşenin gölge kökünde durduğu için standart seçiciler sitekey'i göremez. Test ortamınızda (staging.example.com/qa-login) gölge kökü delip sitekey'i çıkarmak, bu akışı uçtan uca doğrulamanın tek güvenilir yoludur.

Python ve Playwright ile Shadow DOM delme

Playwright'ın >> delici seçicisi ve locator API'si Shadow DOM'u yerel olarak yönetir — gölge sınırlarını elle ele almanız gerekmez. Aşağıdaki script sitekey'i iki yöntemle çıkarır (önce delici seçici, ardından tüm gölge kökleri özyinelemeli tarayan bir page.evaluate), CaptchaAI ile Turnstile token'ını çözer ve token'ı bileşene geri enjekte eder:

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

Node.js ve Puppeteer ile Shadow DOM geçişi

Puppeteer kullanıyorsanız aynı mantık geçerlidir: gölge köklerini özyinelemeli gezin, .cf-turnstile veya .g-recaptcha öğesini bulun, sitekey'i okuyun ve çözülmüş token'ı cf-turnstile-response ya da #g-recaptcha-response alanına yazın.

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

Shadow DOM derinliği: kaç katman delmeniz gerekir?

Kaç gölge katmanı delmeniz gerektiği sayfanın mimarisine bağlıdır. Aşağıdaki tablo yaygın derinlikleri ve her biri için doğru yaklaşımı özetler:

Derinlik Örnek Yaklaşım
Tek seviye <custom-form> #shadow-root > .cf-turnstile Doğrudan gölge kök sorgusu
İki+ seviye <app-shell> #shadow > <login-form> #shadow > .cf-turnstile Özyinelemeli geçiş
Açık gölge kök el.shadowRoot erişilebilir Standart yaklaşım — özyinelemeyi kullanın
Kapalı gölge kök el.shadowRoot null döndürür Delinemez; sayfa yüklenmeden attachShadow({mode:'open'}) geçersiz kılmasıyla açığa zorlayın

Katman katman tanılama: hangi sırayla kontrol edilir

  • Önce açık gölge köklerini, ardından iframe sınırlarını, en son da uygulama düzeyindeki callback'leri veya enjekte edilmiş kancaları kontrol edin.
  • Zorluğun hangi katmanda çıktığını kaydedin; hedef sitedeki ileride yapılacak değişiklikleri böylece daha hızlı fark edersiniz.
  • Kapalı kökler için her zaman bir yedek yol bulundurun — her sayfanın DOM erişimini aynı şekilde açtığını varsaymayın.

Sorun giderme

Gölge kök işlemede en sık karşılaşılan beş sorun ve hızlı çözümleri:

Sorun Sebep Düzeltme
shadowRoot null döndürüyor Kapalı Shadow DOM Sayfa yüklenmeden attachShadow'yi geçersiz kılıp mode: 'open''a zorlayın
Sitekey bulundu ama token reddedildi Çözücüye yanlış pageurl gönderildi Çıkarma anında sabit kodlanmış URL yerine page.url() kullanın
Token enjekte edildi ama form gönderilmiyor CAPTCHA callback'i tetiklenmedi Token değerini ayarladıktan sonra callback fonksiyonunu bulup çağırın
Özyinelemeli arama yavaş Derin DOM ağacı Özyineleme derinliğini sınırlayın; bilinen gölge ana makine etiketlerini hedefleyin
CAPTCHA ilk yüklemede bulunamıyor Widget eşzamansız yükleniyor Önce gölge ana makine öğesini bekleyin: page.waitForSelector('custom-form')

Sık sorulan sorular

CaptchaAI, Shadow DOM içindeki CAPTCHA'yı destekliyor mu?

Evet. CaptchaAI API'si çözümü sunucu tarafında yapar; ona yalnızca sitekey ve pageurl gerekir. CAPTCHA'nın Shadow DOM'da mı yoksa normal DOM'da mı durduğu çözümü etkilemez. Turnstile ile reCAPTCHA v2/v3 GA olarak desteklenir; gölge kök işleme yalnızca sitekey'i canlı sayfadan çıkarmanız veya token'ı geri yazmanız gerektiğinde devreye girer.

Shadow DOM ile iframe aynı şey mi?

Hayır. İkisi de kapsülleme sağlar ama farklı çalışır: iframe ayrı bir belge ve gezinme bağlamı oluşturur, Shadow DOM ise aynı belgede yalıtılmış bir alt ağaç kurar. Bu yüzden çıkarma teknikleri de ayrışır — iframe'lerde çerçeve bağlamına girmeniz, Shadow DOM'da ise gölge kökü delmeniz gerekir.

Hangi web framework'leri CAPTCHA'yı Shadow DOM'a yerleştirir?

En sık Lit, Stencil ve Angular Elements gibi web bileşeni araçlarıyla üretilen bileşenlerde görürsünüz. Ayrıca birçok tasarım sistemi kütüphanesi ve üçüncü taraf form widget'ı, stil çakışmalarını önlemek için tüm bileşeni bir gölge köke sarar. React veya Vue kendisi varsayılan olarak Shadow DOM kullanmaz; kullanılıp kullanılmadığını DevTools'ta #shadow-root etiketiyle doğrulayabilirsiniz.

Kapalı (closed) shadow root'tan sitekey çıkarabilir miyim?

Doğrudan hayır — kapalı bir kök el.shadowRoot erişimini engeller. Pratik çözüm, sayfa yüklenmeden Element.prototype.attachShadow'yi geçersiz kılıp tüm gölge kökleri açık moda zorlamaktır. Bu, Playwright ve Puppeteer'da page.evaluateOnNewDocument ile çalışır.

Tarayıcı çalıştırmadan Shadow DOM CAPTCHA'sını çözebilir miyim?

Çoğu zaman evet. Sitekey'i bir kez öğrendikten sonra (örneğin bir kez tarayıcıyla çıkarıp sabitleyerek) CaptchaAI API'sine yalnızca sitekey ve pageurl göndermeniz yeterlidir; token'ı sunucudan alırsınız. Tarayıcıyla gölge kök gezme yalnızca sitekey her oturumda değişiyorsa veya token'ı canlı DOM'a geri yazmanız gerekiyorsa şarttır.

İlgili makaleler

Sonraki adımlar

Web bileşenlerinin içine gizlenmiş CAPTCHA'ları uçtan uca yönetin: CaptchaAI API anahtarınızı alın ve gölge kök geçişini iş akışınıza ekleyin.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.