Standart document.querySelector gölge köklerin içine ulaşamaz. Bir CAPTCHA widget'ı bir web bileşeninin gölge DOM'unda oluşturulduğunda, her zamanki site anahtarı çıkarma komut dosyanız null değerini döndürür. CAPTCHA orada; seçiciniz onu göremiyor.
CAPTCHA'lar Neden Gölge DOM'da Sonlanıyor?
| Senaryo | Sebep |
|---|---|
| Özel oturum açma bileşeni | Yeniden kullanılabilir bir web bileşeni olarak kapsüllenmiştir |
| Üçüncü taraf form widget'ı | Widget satıcısı tüm formu gölge köküne sarıyor |
| Mikro ön uç mimarisi | Her mikro uygulama izole edilmiş gölge DOM kullanır |
| Tasarım sistemi bileşenleri | CAPTCHA bileşen kitaplığı öğesine gömülü |
Gölge DOM CAPTCHA'larını Algılama
Çözmeden önce CAPTCHA'nın gölge kökün içinde olduğunu doğrulayın:
// 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: Oyun Yazarı Gölge DOM Delici
Oyun Yazarının >> delici seçicisi ve locator API'si gölge DOM'u yerel olarak yönetir:
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: Puppeteer Gölge DOM Geçişi
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();
})();
Gölge DOM Derinliğiyle İlgili Hususlar
| Derinlik | Örnek | Yaklaşım |
|---|---|---|
| 1 seviye | <custom-form> #shadow-root > .cf-turnstile |
Doğrudan gölge kök sorgusu |
| 2+ seviye | <app-shell> #shadow > <login-form> #shadow > .cf-turnstile |
Özyinelemeli geçiş |
| Gölge kökünü aç | el.shadowRoot erişilebilir |
Standart yaklaşım – yinelemeyi kullanın |
| Kapalı gölge kökü | el.shadowRoot null değerini döndürür |
Delinemez; page.evaluate'yi attachShadow({mode:'open'}) geçersiz kılma veya engelleme oluşturma ile birlikte kullanın |
Geri dönüş tanılama sırası
- Önce açık gölge köklerini, ardından iframe sınırlarını ve yalnızca uygulama düzeyindeki geri aramaları veya enjekte edilen kancaları kontrol edin.
- Gelecekteki hedef değişikliklerinin tespit edilmesinin daha kolay olması için hangi katmanın zorluğa maruz kaldığını kaydedin.
- Her sayfanın DOM erişimini eşit şekilde gösterdiğini varsaymak yerine, kapalı kökler için son çare olarak bir yedek bulundurun.
Sorun giderme
| Sorun | Sebep | Düzeltme |
|---|---|---|
shadowRoot, null'yi döndürür |
Kapalı gölge DOM | mode: 'open''yi zorlamak için sayfa yüklenmeden önce attachShadow'yi geçersiz kılın |
| Site anahtarı bulundu ancak jeton reddedildi | Çözücüye yanlış sayfa URL'si gönderildi | Çıkarma sırasında sabit kodlanmış bir URL değil page.url() kullanın |
| Jeton enjekte edildi ancak form gönderilmiyor | CAPTCHA geri araması tetiklenmedi | Belirteç değerini ayarladıktan sonra geri arama işlevini bulun ve çağırın |
| Özyinelemeli arama yavaş | Derin DOM ağacı | Özyineleme derinliğini sınırlayın; bilinen gölge ana makine etiketi adlarını hedefle |
| CAPTCHA ilk yüklemede bulunamadı | Widget eşzamansız olarak yükleniyor | Önce gölge ana bilgisayar öğesini bekleyin: page.waitForSelector('custom-form') |
SSS
CAPTCHA'nın gölge DOM kullanıp kullanmadığını nasıl anlarım?
Chrome DevTools'ta CAPTCHA öğesini inceleyin. CAPTCHA işaretlemesinin üzerinde #shadow-root (open) veya #shadow-root (closed) görüyorsanız, bu bir gölge kökün içindedir. document'den normal querySelector onu bulamayacak.
Gölge DOM geçişini tamamen önleyebilir miyim?
CaptchaAI API'sini doğrudan kullanıyorsanız (tarayıcı enjeksiyonu değil), yalnızca site anahtarına ve sayfa URL'sine ihtiyacınız vardır; gölge DOM önemli değildir. Gölge DOM işleme yalnızca site anahtarlarını canlı sayfadan çıkarmanız veya belirteçleri DOM'a geri eklemeniz gerektiğinde gereklidir.
Peki ya kapalı gölge kökleri?
Kapalı gölge kökleri el.shadowRoot erişimini engeller. Çözüm, sayfa yüklenmeden önce Element.prototype.attachShadow'yi geçersiz kılmak ve tüm gölge kökleri açık moda zorlamaktır. Bu, Oyun Yazarı ve Puppeteer'da page.evaluateOnNewDocument aracılığıyla çalışır.
İlgili Makaleler
- Geetest ve Cloudflare Turnstile Karşılaştırması
- Cloudflare Turnstile 403 Token Düzeltmesinden Sonra
- Cloudflare Turnstile Widget Modlarının Açıklaması
Sonraki Adımlar
Web bileşenlerinin içine gizlenmiş CAPTCHA'ları yönetin -CaptchaAI API anahtarınızı alınve gölge DOM geçişini uygulayın.
İlgili kılavuzlar: