Cara Menemukan Kebocoran Deteksi Browser Headless di Stack Anda

headless browser detection - How to Find Headless Browser Detection Leaks in Your Stack

Deteksi browser headless bukan satu tes tunggal, melainkan tumpukan tes kecil. Sebagian besar setup automation gagal di tiga tes yang sama, bahkan sebelum halaman pertama selesai dimuat. Anda tidak perlu dashboard vendor untuk melihatnya, karena semua yang penting bisa dibaca dari browser yang sudah Anda buka. Artikel ini memberi empat probe, mengurutkan temuannya berdasarkan seberapa cepat masing-masing membuat Anda diblokir, lalu menambal yang memang layak ditambal. Artikel ini juga menarik garis yang sering dilewatkan tulisan lain. Fingerprint yang bersih menurunkan seberapa sering Anda mendapat challenge, dan tidak akan pernah membawanya ke nol.

Apa yang Anda butuhkan

  • Chrome atau Chromium, dan satu cara menjalankan JavaScript di halaman: DevTools secara manual, atau panggilan evaluate dari driver Anda.
  • Driver apa pun yang sudah Anda pakai. Probe-nya JavaScript biasa, jadi Playwright, Puppeteer, dan Selenium berjalan tanpa perubahan.
  • Python 3.10 atau lebih baru kalau Anda ingin menjalankan patch dan contoh solve di bagian akhir.
  • CapSkip terpasang dan berjalan, dan ini hanya untuk bagian terakhir. Ikuti panduan penyiapan dan catat mode koneksi mana yang Anda pilih, karena itu menentukan host mana yang dituju kode Anda.

Ada satu hal yang sebaiknya diputuskan sebelum mulai. Pemecah tidak harus berada di mesin yang sama dengan browser. Mode Local mendengarkan alamat loopback dan hanya melayani perangkat itu, sedangkan mode Server mendengarkan alamat jaringan atau IP publik Anda, sehingga mesin kedua, sebuah VPS, atau runner terkelola bisa menjangkau instance yang sama lewat API. Keduanya dijelaskan di pengaturan koneksi, dan ada bagian singkat soal kasus server di bawah.

Langkah 1: baca sinyal yang menentukan sejak awal

Mulai dari yang murah. Setiap skrip anti-bot komersial membaca ini dalam beberapa milidetik pertama, karena semuanya pembacaan properti yang sinkron dan tanpa biaya jaringan. Tempel ini di console halaman yang benar-benar memblokir Anda, bukan di tab kosong, karena sebagian nilainya bergantung pada dokumen.

// 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

Sesi Chrome yang asli melaporkan properti webdriver sebagai undefined, tiga sampai delapan entri di daftar plugin, minimal dua bahasa yang diterima, jumlah core yang cocok dengan mesinnya, string platform seperti Win32 atau MacIntel, dan objek window.chrome yang terisi lengkap dengan runtime di dalamnya. Container yang diotomatiskan biasanya melaporkan true, nol, satu bahasa, dua core, Linux x86_64, dan sama sekali tidak ada untuk dua poin terakhir.

Properti webdriver sudah banyak dikenal, dan itu satu-satunya isi daftar tadi yang memang ada dengan sengaja. Ini sudah distandardisasi: spesifikasi W3C WebDriver mewajibkan driver yang patuh menyetel sebuah flag, dan properti inilah yang mengeksposnya, sementara MDN mendokumentasikan perilaku yang sama. Jadi keberadaannya bukan bug yang lupa diperbaiki orang. Itu browser yang melakukan tepat seperti yang diperintahkan spesifikasi.

Baca baris-barisnya bersamaan, bukan satu per satu, karena skrip deteksi membandingkannya untuk mencari kecocokan. User agent Windows yang berdampingan dengan string platform Linux adalah sinyal yang jauh lebih kuat daripada salah satu nilainya sendiri, dan justru di situlah hampir semua patch buatan sendiri tersandung lebih dulu.

Langkah 2: cari artefak driver yang tertinggal di window

Chromedriver dan lapisan injeksi Selenium meninggalkan variabel global bernama. Semuanya sangat mudah dienumerasi, tidak ada pengguna wajar yang memilikinya, dan menemukan satu saja sudah bersifat pasti, bukan kemungkinan.

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

Temuan di sini adalah hal paling berharga yang bisa diberikan audit ini, karena tidak ada yang bisa diperdebatkan. Nama acak yang dimulai dengan prefiks cdc adalah penanda klasik Chromedriver, dan jawaban lazimnya begini: berhenti menambal manual, lalu pindah ke build driver yang menghapusnya untuk Anda. Jalur itu kami tulis terpisah di panduan menangani CAPTCHA di undetected-chromedriver, dan gambaran Selenium yang lebih luas ada di halaman pemecah CAPTCHA Selenium , yang membahas opsi driver lebih dalam.

Langkah 3: tanya GPU dia mengira dirinya apa

WebGL melaporkan vendor dan renderer grafis sebagai string biasa, dan container tanpa GPU harus menjawab jujur dengan nama fallback perangkat lunaknya. Ini hal paling berisik yang dibocorkan headless Chrome di Docker, dan tidak ada patch navigator yang bisa menjangkaunya.

// 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.

Melihat SwiftShader bukan hal fatal, dan itu juga tidak bisa dipalsukan secara jujur, karena string renderer palsu tetap harus cocok dengan piksel yang benar-benar dihasilkan canvas Anda. Kalau Anda berjalan di container melawan target yang agresif, pilihan realistisnya dua: beri container itu GPU sungguhan, atau pindahkan pekerjaannya ke mesin yang punya GPU.

Langkah 4: periksa konteks Worker secara terpisah

Probe ini menangkap setup stealth yang setengah jadi, dan hampir tidak ada yang menjalankannya. Patch yang diterapkan ke window halaman tidak menular ke Web Worker, karena Worker mendapat objek navigator yang baru dan bersih, jadi setup yang terlihat bersih di console masih menjawab apa adanya satu lapis di bawahnya.

// 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.

Perlakukan komentar terakhir itu dengan serius, karena yang dinilai skrip semacam ini adalah konsistensi. Browser yang menjawab undefined di satu konteks dan true di konteks lain sudah memberi tahu detektor dua hal: bahwa dia diotomatiskan, dan bahwa ada yang berusaha menyembunyikannya. Hal kedua itulah yang mendorong sesi dari sekadar dinilai menjadi diblokir.

Sinyal deteksi browser headless mana yang paling menentukan

Tidak semua temuan layak menghabiskan sore Anda. Diurutkan berdasarkan seberapa cepat masing-masing bekerja:

SinyalBagaimana sesi asli terlihatBobot
Global driver yang tertinggal di objek windowSama sekali tidak adaKritis: pasti sejak halaman dimuat
Properti webdriver pada navigatorUndefinedKritis: dibaca dalam hitungan milidetik
Kecocokan platform, user agent, dan bahasaKetiganya menggambarkan satu mesinKritis: ketidakcocokan lebih berat dari nilai mana pun
String renderer WebGLGPU yang bernama, bukan fallback perangkat lunakTinggi: challenge dalam hitungan detik
Event sintetis yang melaporkan isTrusted sebagai falseTrue, karena input datang dari browserTinggi: aktif pada interaksi pertama
Fingerprint TLS dan HTTP/2Cocok dengan build Chrome yang Anda akuiTinggi, dan tak terlihat oleh semua probe di atas
Daftar plugin kosong, satu bahasa, dua coreTerisi dan masuk akalSedang: menambah bobot pada skor
Jumlah font dan fingerprint audioSet font desktop, hash audio yang bukan nolSedang: jarang menentukan kalau berdiri sendiri

Baris isTrusted sering disalahpahami, jadi ini perlu dijelaskan tepat. Memanggil metode click sebuah elemen dari JavaScript menghasilkan event dengan isTrusted bernilai false, dan itu mudah terlihat. Klik yang sama lewat Playwright, Puppeteer, atau Selenium tidak begitu, karena semuanya melewati pipeline input milik browser itu sendiri. Jadi ini kebocoran pada scripting DOM buatan tangan, bukan pada driver Anda.

Langkah 5: tambal yang kritis

Dua aturan sebelum menulis kode. Tambal lebih awal, sebelum skrip halaman berjalan, kalau tidak detektor sudah membaca nilai aslinya dan perbaikan Anda datang terlambat. Dan tambal secara sempit, karena override yang kasar justru bisa dideteksi: menimpa fungsi native membuat sumbernya terlihat oleh siapa pun yang memanggil toString padanya, sehingga sinyal yang tersembunyi berubah menjadi sinyal yang jelas.

# 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.

Ada tiga perbaikan murah yang tidak muncul di potongan kode itu, karena sifatnya konfigurasi dan bukan kode. Pakai channel Chrome yang asli daripada Chromium bawaan. Simpan direktori profil yang persisten antar-run, supaya setiap sesi datang dengan cookie dan riwayat, bukan tampak baru lahir. Dan cocokkan locale serta zona waktu dengan tempat asal trafik Anda tampaknya. Ketiganya menggeser skor lebih banyak daripada override navigator apa pun, dan tidak satu pun bisa tertangkap berbohong. Kalau Anda ingin detail per framework, halaman pemecah CAPTCHA Playwright membahas perkabelan di sisi itu, dan pengguna Puppeteer mendapat penjelasan yang sama di halaman pemecah CAPTCHA Puppeteer untuk setelan driver mereka sendiri.

Apa yang tidak diperbaiki oleh semua ini

Tiga kategori berada di luar browser, jadi setiap probe di atas buta terhadapnya. Handshake TLS Anda sudah difingerprint sebelum satu byte JavaScript berjalan, dan itulah sebabnya sebuah HTTP client di Python gagal pada pemeriksaan yang dilewati mulus oleh request yang sama dari Chrome. IP Anda membawa reputasi jaringannya. Dan perilaku Anda dinilai sepanjang sesi: laju request, urutan navigasi, seberapa cepat form terisi. Jadi deteksi browser headless selalu hanya sebagian dari alasan Anda dihentikan.

Semua ini mengubah peluang Anda mendapat challenge dan tidak ada yang menghilangkan challenge. Browser asli di koneksi rumahan pun rutin bertemu reCAPTCHA atau widget Turnstile, karena banyak situs menantang setiap pengunjung di jalur tertentu, apa pun kata skornya.

Memecahkan challenge yang tetap Anda dapatkan

Begitu widget-nya muncul, masalahnya bukan lagi fingerprint melainkan token. CapSkip berjalan di hardware Anda sendiri, menjawab lewat API yang kompatibel dengan 2captcha, dan mengembalikan token yang Anda injeksikan lalu kirim sendiri.

# 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

Karena pemecahnya sebuah daemon lokal dan bukan layanan bermeter, mengulang solve yang gagal tidak berbiaya, dan di sini itu lebih penting daripada kelihatannya. Pekerjaan fingerprint berjalan iteratif, dan memecahkan challenge yang sama empat puluh kali sambil Anda menguji satu patch akan jadi cara debug yang mahal di API yang menagih per pemecahan.

Menjalankan pemecah di server saja

Armada browser biasanya tidak berjalan di laptop, dan pemecahnya juga tidak harus. Pengaturan koneksi menawarkan dua mode, dan satu-satunya perbedaan adalah interface mana yang didengarkan API.

ModeMendengarkan diGunakan saat
Lokal127.0.0.1, hanya perangkat ituBrowser dan pemecah berbagi satu mesin
ServerAlamat jaringan atau IP publik AndaMesin kedua, sebuah VPS, host container, atau runner CI terkelola perlu menjangkaunya

Dalam mode Server, Anda mengarahkan host SDK ke alamat itu alih-alih ke loopback dan tidak mengubah apa pun lagi, sehingga armada dua puluh container bisa berbagi satu pemecah. IP publik statis layak dimiliki kalau pemanggilnya berada di luar jaringan Anda, sebab alamatnya akan masuk ke konfigurasi. Detail lengkapnya ada di pengaturan koneksi dalam panduan penyiapan. Mode Server tetap hardware Anda sendiri dan tetap tanpa meter, jadi yang berubah adalah tempat pemecah berjalan, bukan biayanya.

FAQ

Apakah mode headless yang baru masih bisa dideteksi?

Ya, meski tidak sekasar yang lama. Mode headless Chrome yang lebih baru memakai biner browser biasa, jadi string user agent yang membocorkan identitas dan beberapa API yang dulu hilang kini sudah tidak ada. Properti webdriver tetap disetel, global driver tetap diinjeksikan, dan container tanpa GPU tetap melaporkan renderer perangkat lunak. Headless adalah satu sinyal di antara banyak sinyal, bukan kegagalan seketika.

Apakah plugin stealth saja sudah cukup?

Plugin itu menangani properti JavaScript yang sudah dikenal dengan baik dan memang layak dipakai. Ia tidak bisa menyentuh fingerprint TLS, reputasi IP, atau pola request Anda, dan patch-nya bersifat publik sehingga vendor deteksi mengujinya secara langsung. Jalankan keempat probe setelah memasangnya, jangan menganggap pekerjaannya sudah selesai.

Browser saya berjalan di runner terkelola. Apakah mereka masih bisa menjangkau pemecah?

Ya, dengan CapSkip dalam mode Server. Runner terkelola tidak bisa melihat alamat loopback Anda, jadi ubah pemecah agar mendengarkan alamat jaringan atau IP publik Anda, lalu arahkan host SDK ke sana. Satu instance melayani setiap runner, tidak ada tunnel yang terlibat, dan IP publik statis membuat konfigurasinya stabil. Kedua mode, beserta port yang diikat masing-masing, dijelaskan dalam panduan penyiapan di bagian pengaturan koneksi , tempat cara berpindah di antara keduanya juga dijelaskan.

Apakah fingerprint yang bersih akan menghentikan CAPTCHA?

Ia menguranginya dan tidak akan mengakhirinya. Banyak situs menantang berdasarkan jalur, bukan berdasarkan skor, jadi browser sesempurna apa pun tetap bertemu widget di jalur login atau checkout. Rencanakan keduanya: turunkan laju Anda ditantang, dan simpan sesuatu di pipeline yang menjawab challenge yang tetap datang.

Jalankan probe-nya sebelum Anda mengubah apa pun

Audit deteksi browser headless butuh sekitar sepuluh menit dan biasanya menemukan dua masalah, bukan dua puluh. Bersihkan global driver, buat nilai navigator Anda saling cocok, dan periksa konteks Worker supaya patch Anda tidak saling bertentangan. Setelah itu perlakukan challenge yang tetap datang sebagai pekerjaan terpisah dengan alat terpisah. Menjalankan pemecah captcha di hardware milik Anda sendiri menangani semua itu tanpa tagihan per pemecahan, dan bentuk itulah yang akhirnya diinginkan sebagian besar armada scraping, seperti dijelaskan catatan tentang Pemecah CAPTCHA untuk web scraping untuk sebuah worker pool.