हेडलेस ब्राउज़र डिटेक्शन की लीक अपने स्टैक में कैसे खोजें

हेडलेस ब्राउज़र डिटेक्शन कोई एक टेस्ट नहीं है, छोटे-छोटे टेस्ट का ढेर है। ऑटोमेशन के ज़्यादातर सेटअप उन्हीं तीन में फेल होते हैं, और पहला पेज पूरा लोड होने से पहले ही। इसे देखने के लिए किसी वेंडर के डैशबोर्ड की ज़रूरत नहीं, क्योंकि जो कुछ मायने रखता है वह उसी ब्राउज़र से पढ़ा जा सकता है जो आपके पास पहले से खुला है। यह पोस्ट चार प्रोब देती है, नतीजों को इस क्रम में रखती है कि कौन सा आपको कितनी तेज़ी से ब्लॉक कराता है, और जिन्हें ठीक करना बनता है उन्हें ठीक करती है। साथ में वह लकीर भी खींचती है जिसे बाकी लेख छोड़ देते हैं। साफ़ फ़िंगरप्रिंट यह घटाता है कि आपको कितनी बार चुनौती मिलेगी, और कभी शून्य तक नहीं पहुँचाता।
आपको क्या चाहिए
- Chrome या Chromium, और पेज में JavaScript चलाने का कोई तरीका: हाथ से DevTools, या आपके ड्राइवर का evaluate कॉल।
- कोई भी ड्राइवर जो आप पहले से इस्तेमाल करते हैं। प्रोब सादा JavaScript हैं, इसलिए Playwright, Puppeteer और Selenium बिना किसी बदलाव के चलते हैं।
- Python 3.10 या नया, अगर आप आखिर में दिया पैच और सॉल्व का उदाहरण चलाना चाहते हैं।
- CapSkip इंस्टॉल और चालू, और यह सिर्फ़ आखिरी सेक्शन के लिए चाहिए। यह गाइड देखें: सेटअप गाइड । और यह नोट कर लें कि आपने कौन सा कनेक्शन मोड चुना है, क्योंकि वही तय करता है कि आपका कोड किस host की तरफ़ जाएगा।
शुरू करने से पहले एक बात तय कर लेना ठीक रहता है। सॉल्वर का उसी मशीन पर होना ज़रूरी नहीं जिस पर ब्राउज़र है। लोकल मोड loopback पते पर सुनता है और सिर्फ़ उसी डिवाइस को सेवा देता है, जबकि सर्वर मोड आपके नेटवर्क या पब्लिक IP पर सुनता है, जिससे दूसरी मशीन, कोई VPS या होस्टेड रनर उसी इंस्टैंस तक API से पहुँच सकता है। दोनों यहाँ बताए गए हैं: कनेक्शन सेटिंग्स। और सर्वर वाले मामले पर नीचे एक छोटा सेक्शन है।
स्टेप 1: वे सिग्नल पढ़ें जो शुरुआत में ही फ़ैसला कर देते हैं
सस्ते वालों से शुरू करें। हर कमर्शियल एंटी-बॉट स्क्रिप्ट इन्हें पहले कुछ मिलीसेकंड में पढ़ लेती है, क्योंकि ये बिना नेटवर्क खर्च वाले सिंक्रोनस प्रॉपर्टी लुकअप हैं। इसे किसी खाली टैब में नहीं, बल्कि उस पेज के कंसोल में पेस्ट करें जो आपको असल में ब्लॉक कर रहा है, क्योंकि कुछ वैल्यू डॉक्युमेंट पर निर्भर करती हैं।
// Paste into DevTools, or hand it to your driver's evaluate call
// so it runs in the real page context rather than a fresh tab.
const leaks = {
webdriver: navigator.webdriver,
plugins: navigator.plugins.length,
languages: navigator.languages.join(","),
cores: navigator.hardwareConcurrency,
memory: navigator.deviceMemory,
platform: navigator.platform,
hasChrome: !!window.chrome,
hasRuntime: !!(window.chrome && window.chrome.runtime),
};
console.table(leaks); // read every row, not just the firstअसली Chrome सेशन webdriver प्रॉपर्टी को undefined बताता है, प्लगइन लिस्ट में तीन से आठ एंट्री, कम से कम दो स्वीकृत भाषाएँ, मशीन से मेल खाती कोर संख्या, Win32 या MacIntel जैसी प्लेटफ़ॉर्म स्ट्रिंग, और भरा हुआ window.chrome ऑब्जेक्ट जिसमें runtime मौजूद हो। ऑटोमेटेड कंटेनर आम तौर पर true, शून्य, एक भाषा, दो कोर, Linux x86_64 बताता है, और आखिरी दो के लिए कुछ भी नहीं।
webdriver प्रॉपर्टी के बारे में लोग पहले से जानते हैं, और उस लिस्ट में यही एक चीज़ है जो जान-बूझकर वहाँ है। यह मानक का हिस्सा है: W3C WebDriver स्पेसिफ़िकेशन के मुताबिक हर अनुरूप ड्राइवर को वह flag सेट करना होता है जिसे यही प्रॉपर्टी बाहर दिखाती है, और MDN भी यही व्यवहार दर्ज करता है। तो यह किसी की भूली हुई गड़बड़ नहीं है। ब्राउज़र ठीक वही कर रहा है जो स्पेसिफ़िकेशन उसे करने को कहती है।
पंक्तियों को एक-एक करके नहीं, साथ में पढ़ें, क्योंकि डिटेक्शन स्क्रिप्ट इन्हें आपसी मेल के लिए मिलाती हैं। Linux प्लेटफ़ॉर्म स्ट्रिंग के बगल में बैठा Windows user agent दोनों में से किसी एक वैल्यू से कहीं ज़्यादा तेज़ सिग्नल है, और लगभग हर हाथ से बने पैच की पहली गलती यही होती है।
स्टेप 2: window में छूटे ड्राइवर के निशान खोजें
Chromedriver और Selenium की इंजेक्शन लेयर अपने पीछे नाम वाले ग्लोबल छोड़ जाती हैं। इन्हें गिनना बहुत आसान है, किसी सामान्य उपयोगकर्ता के पास ये नहीं होते, और एक भी मिल जाए तो यह संभावना नहीं, पक्की बात होती है।
// Injected globals from Chromedriver and Selenium. A clean
// browser prints the word clean and nothing else.
const prefixes = ["__cdc", "__selenium", "__webdriver", "__driver"];
const found = Object.keys(window).filter(
(k) => prefixes.some((p) => k.startsWith(p))
);
// Two more that Chromedriver adds under fixed names.
for (const name of ["domAutomation", "domAutomationController"]) {
if (window[name] !== undefined) found.push(name);
}
console.log(found.length ? found : "clean");यहाँ मिला एक भी निशान इस पूरे ऑडिट का सबसे कीमती नतीजा है, क्योंकि इसमें बहस की कोई गुंजाइश नहीं। cdc उपसर्ग से शुरू होने वाला कोई रैंडम नाम Chromedriver का क्लासिक मार्कर है, और इसका आम जवाब यही है: हाथ से पैच करना छोड़ दें और ऐसा ड्राइवर बिल्ड लें जो इसे आपके लिए हटा दे। यह रास्ता हमने अलग से इस लेख में लिखा है: undetected-chromedriver में कैप्चा कैसे संभालें। और Selenium की बड़ी तस्वीर इस पेज पर है: Selenium कैप्चा सॉल्वर । ड्राइवर के विकल्प वहाँ और विस्तार से देखे गए हैं।
स्टेप 3: GPU से पूछें कि वह खुद को क्या समझता है
WebGL ग्राफ़िक्स वेंडर और renderer को सादी स्ट्रिंग के रूप में बताता है, और जिस कंटेनर में GPU नहीं है उसे ईमानदारी से अपने सॉफ़्टवेयर फ़ॉलबैक का नाम बताना पड़ता है। Docker में चल रहा हेडलेस Chrome सबसे ज़ोर से यही चीज़ खोलता है, और navigator की कोई पैचिंग यहाँ तक नहीं पहुँचती।
// The renderer is exposed as a plain string, so read it directly.
const gl = document.createElement("canvas").getContext("webgl");
const dbg = gl.getExtension("WEBGL_debug_renderer_info");
console.log(
gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL),
gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)
);
// SwiftShader, llvmpipe, Mesa or VMware means no real GPU here.SwiftShader दिखना जानलेवा नहीं है, और इसे ईमानदारी से छिपाया भी नहीं जा सकता, क्योंकि नकली renderer स्ट्रिंग को भी उन पिक्सल से मेल खाना पड़ता है जो आपका canvas असल में बनाता है। अगर आप कंटेनर में किसी आक्रामक टारगेट के सामने हैं, तो व्यावहारिक विकल्प दो हैं: उस कंटेनर को असली GPU देना, या काम को ऐसी मशीन पर ले जाना जिसके पास GPU हो।
स्टेप 4: Worker कॉन्टेक्स्ट अलग से जाँचें
यह प्रोब आधे-अधूरे stealth सेटअप पकड़ता है, और इसे लगभग कोई नहीं चलाता। पेज के window पर लगाए पैच Web Worker तक नहीं पहुँचते, क्योंकि उसे अपना नया navigator ऑब्जेक्ट मिलता है; इसलिए जो सेटअप कंसोल में साफ़ दिखता है, वह एक परत नीचे अब भी सच बोल रहा होता है।
// A Worker gets its own navigator, untouched by page patches.
const src = "postMessage(navigator.webdriver)";
const url = URL.createObjectURL(new Blob([src]));
new Worker(url).onmessage = (e) => console.log("worker says:", e.data);
// true here while the page says undefined is a mismatch,
// and a mismatch is a worse signal than either value alone.उस आखिरी कमेंट को गंभीरता से लें, क्योंकि ये स्क्रिप्ट जिस चीज़ को नंबर देती हैं वह है आपसी मेल। जो ब्राउज़र एक जगह undefined और दूसरी जगह true कहता है, उसने डिटेक्टर को दो बातें बता दीं: कि वह ऑटोमेटेड है, और कि किसी ने इसे छिपाने की कोशिश की। दूसरी बात ही सेशन को स्कोर होने से ब्लॉक होने तक धकेलती है।
हेडलेस ब्राउज़र डिटेक्शन में कौन से सिग्नल सबसे ज़्यादा मायने रखते हैं
हर नतीजा आपकी पूरी दोपहर का हक़दार नहीं है। कौन कितनी तेज़ी से असर करता है, उस क्रम में:
| सिग्नल | असली सेशन कैसा दिखता है | वज़न |
|---|---|---|
| window ऑब्जेक्ट में छूटे ड्राइवर ग्लोबल | एक भी मौजूद नहीं | क्रिटिकल: लोड होते ही पक्का नतीजा |
| navigator की webdriver प्रॉपर्टी | Undefined | क्रिटिकल: मिलीसेकंड में पढ़ी जाती है |
| प्लेटफ़ॉर्म, user agent और भाषा का आपसी मेल | तीनों एक ही मशीन बताते हैं | क्रिटिकल: बेमेल होना किसी एक वैल्यू से भारी है |
| WebGL renderer स्ट्रिंग | नाम वाला GPU, सॉफ़्टवेयर फ़ॉलबैक नहीं | ऊँचा: सेकंडों में चुनौती |
| isTrusted को false बताने वाले सिंथेटिक इवेंट | True, क्योंकि इनपुट ब्राउज़र से आया | ऊँचा: पहले ही इंटरैक्शन पर चलता है |
| TLS और HTTP/2 फ़िंगरप्रिंट | उस Chrome बिल्ड से मेल खाता है जिसका आप दावा करते हैं | ऊँचा, और ऊपर की किसी भी प्रोब को नहीं दिखता |
| खाली प्लगइन लिस्ट, एक भाषा, दो कोर | भरा हुआ और भरोसेमंद | मध्यम: स्कोर में जुड़ता है |
| फ़ॉन्ट की गिनती और ऑडियो फ़िंगरप्रिंट | डेस्कटॉप जैसा फ़ॉन्ट सेट, शून्य से अलग ऑडियो हैश | मध्यम: अकेले कम ही फ़ैसला करता है |
isTrusted वाली पंक्ति को अक्सर गलत समझा जाता है, इसलिए इसे साफ़ कहना ज़रूरी है। किसी एलिमेंट का click मेथड JavaScript से कॉल करने पर जो इवेंट बनता है उसका isTrusted false होता है, और यह पकड़ में आसानी से आता है। वही क्लिक Playwright, Puppeteer या Selenium से कराने पर ऐसा नहीं होता, क्योंकि वे ब्राउज़र की अपनी इनपुट पाइपलाइन से गुज़रते हैं। यानी यह लीक आपके ड्राइवर की नहीं, हाथ से लिखी DOM स्क्रिप्टिंग की है।
स्टेप 5: क्रिटिकल वालों को पैच करें
कोड से पहले दो नियम। जल्दी पैच करें, यानी पेज की स्क्रिप्ट चलने से पहले, वरना डिटेक्टर मूल वैल्यू पढ़ लेगा और आपकी मरम्मत देर से पहुँचेगी। और सीमित दायरे में पैच करें, क्योंकि भोंडा ओवरराइड खुद पकड़ में आ जाता है: नेटिव फ़ंक्शन पर लिखने से उसका सोर्स हर उस व्यक्ति को दिख जाता है जो उस पर toString कॉल करे, और छिपा सिग्नल साफ़ दिखने वाला सिग्नल बन जाता है।
# pip install playwright
from playwright.sync_api import sync_playwright
PATCH = """
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
window.chrome = window.chrome || { runtime: {} };
"""
with sync_playwright() as p:
# Real Chrome leaks less than the bundled Chromium build.
browser = p.chromium.launch(channel="chrome", headless=False)
page = browser.new_page(locale="en-US")
# add_init_script runs before any page script reads navigator.
page.add_init_script(PATCH)
page.goto("https://example.com/page-with-recaptcha")
# Re-run the Step 1 probe here to confirm the patch landed.उस स्निपेट में तीन सस्ते फ़ायदे नहीं हैं, क्योंकि वे कोड नहीं कॉन्फ़िगरेशन हैं। साथ आए Chromium की जगह असली Chrome चैनल इस्तेमाल करें। रन के बीच एक स्थायी प्रोफ़ाइल डायरेक्टरी रखें, ताकि सेशन कुकी और हिस्ट्री के साथ आए, नवजात जैसा न लगे। और locale तथा टाइमज़ोन को उस जगह से मिलाएँ जहाँ से आपका ट्रैफ़िक आता दिखता है। ये तीन मिलकर किसी भी navigator ओवरराइड से ज़्यादा स्कोर हिलाते हैं, और इनमें से किसी को झूठ बोलते नहीं पकड़ा जा सकता। अगर आपको फ़्रेमवर्क की बारीकियाँ चाहिए, तो यह पेज देखें: Playwright कैप्चा सॉल्वर । इस तरफ़ की वायरिंग वही बताता है, और Puppeteer वाले वही तरीका इस पेज पर पाएँगे: Puppeteer कैप्चा सॉल्वर । दोनों अपने-अपने ड्राइवर सेटअप के हिसाब से लिखे गए हैं।
इनमें से कोई भी चीज़ जो ठीक नहीं करती
तीन श्रेणियाँ ब्राउज़र के बाहर बैठती हैं, इसलिए ऊपर की हर प्रोब उनके लिए अंधी है। आपका TLS हैंडशेक फ़िंगरप्रिंट में बदल जाता है, और वह भी JavaScript का एक बाइट चलने से पहले; इसीलिए Python का HTTP क्लाइंट उन जाँचों में फेल होता है जिनसे वही रिक्वेस्ट Chrome से आसानी से निकल जाती है। आपके IP के साथ उसके नेटवर्क की साख जुड़ी होती है। और आपके बर्ताव को पूरे सेशन में नंबर दिए जाते हैं: रिक्वेस्ट की दर, नेविगेशन का क्रम, फ़ॉर्म कितनी तेज़ी से भरे जाते हैं। तो हेडलेस ब्राउज़र डिटेक्शन हमेशा उस वजह का एक हिस्सा ही होता है जिससे आपको रोका गया।
यह सब मिलकर चुनौती मिलने की संभावना बदलता है, और इनमें से कोई चुनौतियाँ खत्म नहीं करता। घरेलू कनेक्शन पर चल रहा असली ब्राउज़र भी नियमित रूप से reCAPTCHA या Turnstile विजेट से टकराता है, क्योंकि बहुत सी साइटें कुछ रास्तों पर हर विज़िटर को जाँचती हैं, स्कोर चाहे जो कहे।
जो चुनौती फिर भी मिलती है, उसे सॉल्व करना
विजेट दिख जाने के बाद समस्या फ़िंगरप्रिंट की नहीं, टोकन की हो जाती है। CapSkip आपके ही हार्डवेयर पर चलता है, 2captcha से मेल खाते API पर जवाब देता है, और ऐसा टोकन लौटाता है जिसे आप खुद इंजेक्ट करके भेजते हैं।
# pip install capskip
from capskip import CapSkip
# Local mode. In Server mode this is the solver box's address.
solver = CapSkip(host="127.0.0.1", port=8080)
# One call covers v2, Invisible, Enterprise and v3 as options.
result = solver.recaptcha(
sitekey="YOUR_SITEKEY",
url="https://example.com/page-with-recaptcha",
)
# Inject the token into the field the page submits with the form.
page.evaluate(
"t => document.getElementById('g-recaptcha-response').value = t",
result["code"],
)
print(result["code"][:24]) # token, ready to submitसॉल्वर मीटर वाली सेवा नहीं, लोकल डीमन है, इसलिए फेल हुए सॉल्व को दोबारा आज़माने का कोई खर्च नहीं, और यहाँ यह बात दिखने से ज़्यादा मायने रखती है। फ़िंगरप्रिंट का काम दोहराव में आगे बढ़ता है, और एक पैच जाँचते हुए उसी चुनौती को चालीस बार सॉल्व करना, हर सॉल्व पर पैसे लेने वाले API पर डीबग करने का महँगा तरीका होता।
सॉल्वर को उसकी जगह सर्वर पर चलाना
ब्राउज़र के बेड़े आम तौर पर लैपटॉप पर नहीं चलते, और सॉल्वर को भी वहाँ होना ज़रूरी नहीं। कनेक्शन सेटिंग्स में दो मोड हैं, और फ़र्क़ सिर्फ़ इतना है कि API किस इंटरफ़ेस पर सुनता है।
| मोड | किस पर सुनता है | कब इस्तेमाल करें |
|---|---|---|
| लोकल | 127.0.0.1, केवल उसी डिवाइस पर | ब्राउज़र और सॉल्वर एक ही मशीन साझा करते हैं |
| सर्वर | आपका नेटवर्क पता या पब्लिक IP | दूसरी मशीन, कोई VPS, कंटेनर होस्ट या होस्टेड CI रनर को इस तक पहुँचना है |
सर्वर मोड में आप SDK का host loopback की जगह उस पते पर कर देते हैं और बाकी कुछ नहीं बदलते, जिससे बीस कंटेनर का बेड़ा एक ही सॉल्वर साझा कर सकता है। अगर कॉल करने वाले आपके नेटवर्क से बाहर बैठे हैं तो स्टैटिक पब्लिक IP रखना फ़ायदेमंद है, क्योंकि पता कॉन्फ़िगरेशन में चला जाता है। पूरी जानकारी सेटअप गाइड के इस सेक्शन में है: कनेक्शन सेटिंग्स । सर्वर मोड अब भी आपका ही हार्डवेयर है और अब भी बिना मीटर, यानी यह बदलता है कि सॉल्वर कहाँ चलता है, उसकी लागत के बारे में कुछ नहीं।
FAQ
क्या नया हेडलेस मोड अब भी पकड़ में आता है?
हाँ, पर पुराने जितने भोंडे तरीके से नहीं। Chrome का नया हेडलेस मोड सामान्य ब्राउज़र बाइनरी ही इस्तेमाल करता है, इसलिए भेद खोलने वाली user agent स्ट्रिंग और कई गायब API अब नहीं हैं। webdriver प्रॉपर्टी अब भी सेट होती है, ड्राइवर ग्लोबल अब भी इंजेक्ट होते हैं, और GPU रहित कंटेनर अब भी सॉफ़्टवेयर renderer बताता है। हेडलेस होना तुरंत फेल होना नहीं, कई सिग्नल में से एक है।
क्या अकेला stealth प्लगइन काफ़ी है?
जानी-मानी JavaScript प्रॉपर्टीज़ को वह ठीक संभालता है और उसे इस्तेमाल करना चाहिए। आपके TLS फ़िंगरप्रिंट, IP की साख और रिक्वेस्ट के पैटर्न तक वह नहीं पहुँचता, और उसके पैच सार्वजनिक हैं, इसलिए डिटेक्शन वेंडर सीधे उन्हीं के खिलाफ़ टेस्ट करते हैं। एक इंस्टॉल करने के बाद काम पूरा मान लेने की जगह चारों प्रोब चलाएँ।
मेरे ब्राउज़र होस्टेड रनर पर चलते हैं। क्या वे सॉल्वर तक पहुँच पाएँगे?
हाँ, अगर CapSkip सर्वर मोड में हो। होस्टेड रनर आपका loopback पता नहीं देख सकता, इसलिए सॉल्वर को अपने नेटवर्क या पब्लिक IP पर सुनने के लिए बदलें और SDK का host उसी पर करें। एक इंस्टैंस हर रनर को सेवा देता है, कोई टनल नहीं लगती, और स्टैटिक पब्लिक IP कॉन्फ़िगरेशन को स्थिर रखता है। दोनों मोड, और हर एक जिस पोर्ट पर बँधता है, सेटअप गाइड में इस सेक्शन में बताए गए हैं: कनेक्शन सेटिंग्स । दोनों के बीच स्विच करना भी वहीं समझाया गया है।
क्या साफ़ फ़िंगरप्रिंट कैप्चा बंद करा देगा?
वह उन्हें घटाता है, खत्म नहीं करता। बहुत सी साइटें स्कोर के बजाय रास्ते के हिसाब से जाँचती हैं, इसलिए बेदाग़ ब्राउज़र भी लॉगिन या चेकआउट के रास्ते पर विजेट से टकराता है। दोनों के लिए योजना रखें: जिस दर पर आपको जाँचा जाता है उसे घटाएँ, और पाइपलाइन में कुछ ऐसा रखें जो फिर भी आने वालों का जवाब दे।
कुछ बदलने से पहले प्रोब चलाएँ
हेडलेस ब्राउज़र डिटेक्शन का ऑडिट लगभग दस मिनट लेता है और आम तौर पर बीस नहीं, दो समस्याएँ निकालता है। ड्राइवर ग्लोबल साफ़ करें, अपनी navigator वैल्यू को आपस में मिलाएँ, और Worker कॉन्टेक्स्ट जाँचें ताकि आपके पैच खुद अपना खंडन न करें। फिर जो चुनौतियाँ तब भी आती रहें, उन्हें अलग टूल वाला अलग काम मानें। अपने ही हार्डवेयर पर चलने वाला कैप्चा सॉल्वर उन्हें हर सॉल्व पर बिल चुकाए बिना निपटा देता है, और आखिर में ज़्यादातर स्क्रैपिंग बेड़ों को यही ढाँचा चाहिए होता है; यह लेख वेब स्क्रैपिंग के लिए कैप्चा सॉल्वर को एक worker pool के हिसाब से समझाता है।
