การตรวจจับ Puppeteer ทำงานอย่างไรจริง ๆ: 12 สัญญาณที่เปิดโปงบอท
เฟรมเวิร์ก automation สืบทอด fingerprint ของเบราว์เซอร์จริง แต่เปลี่ยนมันในหลายสิบจุดที่สังเกตได้ คู่มือ 12 สัญญาณด้าน JavaScript เครือข่าย และพฤติกรรม ที่ผู้ป้องกันใช้จับ Puppeteer และ Playwright
เฟรมเวิร์ก automation ควบคุมเบราว์เซอร์จริง ซึ่งหมายความว่ามันสืบทอด fingerprint ของเบราว์เซอร์มาด้วย Chrome ที่ขับโดย Puppeteer มี user agent เดียวกัน, เวอร์ชัน Chrome เดียวกัน และ Chromium build เดียวกันกับ Chrome ของมนุษย์ เมื่อมองผิวเผิน ทั้งคู่ดูเหมือนกันทุกประการ
แต่เฟรมเวิร์ก automation เปลี่ยนเบราว์เซอร์ในหลายสิบจุดเล็ก ๆ น้อย ๆ — ส่วนใหญ่สังเกตได้จาก JavaScript การตรวจจับไม่ใช่เวทมนตร์ มันคือเช็กลิสต์
1. แฟลก navigator.webdriver
สัญญาณที่ง่ายที่สุดและโด่งดังที่สุด เมื่อ Chrome ทำงานภายใต้เฟรมเวิร์ก automation navigator.webdriver จะคืนค่า true มันเป็นส่วนหนึ่งของสเปก W3C WebDriver และ Chromium ก็ implement มันตามค่าเริ่มต้น
โปรเจกต์ automation ที่จริงจังทุกตัวแพตช์สิ่งนี้ภายในไม่กี่วินาที Puppeteer-extra-stealth override ตัว getter ส่วนผู้ใช้ Playwright inject สคริปต์ Object.defineProperty บนเอกสารใหม่ แฟลกนี้ซ่อนได้ง่ายมาก
แต่ข้อเท็จจริงที่ว่ามันซ่อนได้ง่ายก็เป็นสัญญาณในตัวมันเอง Chrome ที่ถูกต้องตามกฎหมายไม่จำเป็นต้องซ่อนอะไร เมื่อหน้าเว็บตรวจสอบ Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') แล้วตัว descriptor ดูต่างจากของเบราว์เซอร์จริง — automation ก็ถูกจับได้แม้จะแพตช์แฟลกไปแล้ว
2. ความผิดปกติของออบเจ็กต์ chrome
Chrome ที่ถูกต้องตามกฎหมายเปิดเผยออบเจ็กต์ global window.chrome ที่มีโครงสร้างมากมาย — chrome.runtime, chrome.loadTimes, chrome.csi ส่วน Headless Chrome และการตั้งค่า Puppeteer รุ่นเก่ามักละเว้นออบเจ็กต์นี้ทั้งหมดหรือเปิดเผยแบบตัดทอน
Stealth plugin สร้างออบเจ็กต์นี้ขึ้นมาใหม่ แต่การสร้างใหม่ไม่สมบูรณ์แบบ chrome.runtime.onConnect อาจมีอยู่โดยไม่มี chrome.runtime.PlatformOs ลายเซ็นของฟังก์ชันอาจคืนค่าออบเจ็กต์แทนที่จะเป็น undefined ทุกความไม่ตรงกันคือสัญญาณบวก
3. ความไม่สอดคล้องของ Permissions API
เบราว์เซอร์จริงคืนผลลัพธ์ที่สอดคล้องกันเมื่อสอบถามผ่าน Permissions API navigator.permissions.query({name: 'notifications'}) ควรคืนค่า 'default' หากผู้ใช้ยังไม่ได้อนุญาตหรือปฏิเสธสิทธิ์อย่างชัดเจน
Headless Chrome คืนค่า 'denied' ตามค่าเริ่มต้น เพราะไม่มี UI ที่จะแสดง prompt ขอสิทธิ์ เฟรมเวิร์ก automation แพตช์สิ่งนี้ — แต่มักทำไม่ถูกต้อง ตัวบ่งชี้ที่พบบ่อย: การสอบถามสิทธิ์ที่ผิดปกติอย่าง 'clipboard-read' คืนผลลัพธ์ที่ไม่ตรงกับที่ Chrome จริงทำในเวอร์ชันปัจจุบัน
4. อาร์เรย์ plugin และ MIME type
navigator.plugins ในเบราว์เซอร์จริงคืนค่า PluginArray ที่มีรายการอย่าง PDF viewer และ Chromium PDF plugin ในโหมด headless อาร์เรย์นี้ว่างเปล่า
Stealth plugin เพิ่มรายการปลอมเข้าไป แต่รายการเหล่านั้นมักมี property ที่ผิด — ขาด length, ฟิลด์ description ผิด หรือออบเจ็กต์ plugin ที่ไม่ทำตัวเหมือน Plugin instance เมื่อทดสอบด้วย instanceof
5. ความไม่ตรงกันของภาษาและ locale
navigator.language และ navigator.languages ควรตรงกับ HTTP header ชื่อ Accept-Language และควรสอดคล้องกับ timezone ที่รายงานโดย Intl.DateTimeFormat().resolvedOptions().timeZone ด้วย
บอทที่อ้างว่าอยู่ในเคียฟ (timezone Europe/Kyiv) แต่ส่ง Accept-Language: en-US,en;q=0.9 และรายงาน navigator.language === 'en-US' นั้นเป็นไปได้แต่ผิดปกติในเชิงสถิติ เมื่อรวมกับ IP จากดาต้าเซ็นเตอร์ในแฟรงก์เฟิร์ต ภาพก็ชัดเจนขึ้น
6. สตริง WebGL renderer
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) คืนค่าผู้ผลิตและรุ่นของ GPU บนเครื่องจริง ค่านี้อาจเป็น:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Headless Chrome ที่ทำงานในคอนเทนเนอร์มักคืนค่า:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
สตริง GPU ใด ๆ ที่มี SwiftShader หรือ Subzero คือสัญญาณบอทที่แรง
7. ความผิดปกติของ screen และ viewport
หน้าต่างเบราว์เซอร์จริงมี window.outerWidth และ window.outerHeight ที่รวมส่วน chrome ของเบราว์เซอร์ — แถบที่อยู่, แท็บ, แถบบุ๊กมาร์ก ความต่างระหว่างขนาด outer และ inner โดยทั่วไปอยู่ที่ 80–140 พิกเซลในแนวตั้ง
เบราว์เซอร์แบบ headless มักมี outerHeight === innerHeight เพราะไม่มีส่วน chrome ให้แสดง Puppeteer ที่ตั้ง headless: false แก้ปัญหานี้ได้ แต่การตั้งค่า automation จำนวนมากยังคงทำงานด้วยขนาดที่เท่ากัน
8. การแจกแจงฟอนต์
ผู้ใช้จริงมีฟอนต์ติดตั้งเป็นร้อย ๆ ตัว แตกต่างกันตาม OS, ภาษา และแอปพลิเคชันที่ติดตั้ง Windows 11 ที่ติดตั้ง Office มีฟอนต์กว่า 500 ตัว คอนเทนเนอร์ automation ที่รัน Alpine Linux มี 30 ตัว
การแจกแจงฟอนต์ผ่าน document.fonts.check() หรือการเรนเดอร์ผ่าน offscreen canvas เปิดเผยสิ่งนี้ทันที เบราว์เซอร์ที่รายงานตัวเองว่าเป็น Windows 11 Chrome แต่เปิดเผยฟอนต์เพียง 30 ตัวแทบจะแน่นอนว่าเป็น automation
9. ความสุ่มของการเคลื่อนเมาส์
ผู้ใช้จริงขยับเมาส์แบบมีการสั่นไหว เส้นโค้งไม่เป็นเชิงเส้น ความเร็วแปรผัน ระหว่างการคลิกสองครั้ง โดยทั่วไปจะมีเหตุการณ์ mousemove เป็นร้อย ๆ ครั้ง
เฟรมเวิร์ก automation สังเคราะห์การเคลื่อนไหว — ไม่ว่าจะด้วยเส้นตรงไปยังพิกัด หรือด้วยเส้นโค้งที่โปรแกรมไว้ซึ่งขาดการสั่นไหวเล็ก ๆ ของมือมนุษย์ แม้แต่การจำลองด้วยเส้นโค้ง Bezier ก็มักมีโปรไฟล์ความเร่งที่ราบรื่นจนน่าสงสัย
ระบบตรวจจับบอทเก็บร่องรอยการเคลื่อนไหวและให้คะแนนเทียบกับโมเดลการเคลื่อนไหวของมนุษย์ ร่องรอยที่มีความเร็วสม่ำเสมอเกินไป, เส้นโค้งสะอาดเกินไป หรือขาด microevent จะถูกทำเครื่องหมาย
10. ลายเซ็นด้านจังหวะเวลา
เฟรมเวิร์ก automation รัน JavaScript ด้วยลักษณะด้านจังหวะเวลาที่ต่างจากเบราว์เซอร์ที่ขับโดยผู้ใช้ จังหวะของ performance.now() ระหว่างเหตุการณ์ โดยเฉพาะระหว่าง pointerdown กับ pointerup เป็นไปตามการกระจายที่ต่างออกไป
เวลากดค้างคลิกจริงอยู่ที่ 50–150 มิลลิวินาที โดยมีความแปรปรวนสูง ส่วนค่ากดค้างคลิกเริ่มต้นของ Puppeteer เป็นค่าคงที่ — มักเป็น 30 มิลลิวินาทีหรือ 100 มิลลิวินาที — โดยแทบไม่มีความแปรปรวน การคลิกร้อยครั้งที่มีเวลากดค้างเท่ากันเป๊ะคือ automation อย่างชัดเจน
11. ความไม่ตรงกันของ TLS fingerprint
สัญญาณนี้ข้าม JavaScript ไปโดยสิ้นเชิง เมื่อ Puppeteer เชื่อมต่อ มันใช้ TLS stack ของ Chromium — เหมือนกับ Chrome จริง แต่ scraper ที่อิงกับ undici หรือ axios ซึ่งแกล้งทำเป็น Chrome ผ่านการปลอม user agent ใช้ TLS stack ของ Node.js ซึ่งมีลายเซ็น Client Hello (แฮช JA4) ที่ต่างออกไปอย่างชัดเจน
คำขอที่อ้างว่า User-Agent: Chrome/124.0.6367.60 แต่แสดง TLS fingerprint ของ Node.js คือการยืนยันทันทีว่าเป็นไคลเอนต์ที่ไม่ใช่เบราว์เซอร์
12. การรั่วไหลของโปรโตคอล CDP
Puppeteer และ Playwright สื่อสารกับ Chrome ผ่าน Chrome DevTools Protocol ในบางการตั้งค่า โปรโตคอลนี้ทิ้งร่องรอยที่ตรวจจับได้ — property พิเศษบนออบเจ็กต์ Runtime, รูปแบบ Error.stack ที่ถูกเปลี่ยน หรือเอาต์พุต console.debug เฉพาะที่ปรากฏเมื่อมีไคลเอนต์ CDP เชื่อมต่ออยู่เท่านั้น
คำสั่ง Runtime.enable โดยเฉพาะเปลี่ยนวิธีการเรนเดอร์ stack trace บอทที่ไม่เคยทริกเกอร์ error จะหลบสัญญาณนี้ได้ แต่ flow automation ใด ๆ ที่เจอ exception (และส่วนใหญ่ก็เจอ) จะทิ้ง fingerprint ไว้
การซ้อนชั้นสัญญาณเหล่านี้
ไม่มีสัญญาณเดียวใดที่พิสูจน์ automation ได้ ผู้ใช้จริงที่มีไดรเวอร์ GPU ผิดปกติอาจคืนค่าสตริง WebGL แปลก ๆ ผู้ใช้ที่ให้ความสำคัญกับความเป็นส่วนตัวอาจดัดแปลง navigator.plugins
ความแข็งแกร่งของการตรวจจับสมัยใหม่มาจากการรวมสัญญาณเข้าด้วยกัน — สัญญาณบวกอ่อน ๆ 12 ตัวแข็งแกร่งกว่าสัญญาณบวกแรงเพียงตัวเดียว เพราะผู้ใช้จริงแทบไม่ทริกเกอร์ความผิดปกติเกิน 2–3 อย่างพร้อมกัน
ระบบตรวจจับกำหนดน้ำหนักให้แต่ละสัญญาณและคำนวณคะแนน เมื่อเกินเกณฑ์ ผู้เข้าชมจะถูกจัดว่าเป็น automation เกณฑ์และน้ำหนักที่แน่นอนคือส่วนที่ยากที่สุดในการทำให้ถูกต้อง — เข้มเกินไปแล้วผู้ใช้ที่ถูกต้องจะถูกบล็อก, หย่อนเกินไปแล้วบอทที่ซับซ้อนก็จะเล็ดลอดผ่าน
ตัวสัญญาณเองเป็นส่วนที่ง่าย เกือบทุกเฟรมเวิร์ก automation รั่วทั้ง 12 ตัว งานวิศวกรรมที่แท้จริงคือการตัดสินว่าชุดผสมใดสำคัญ, ชุดใดเป็น false positive และจะอัปเดตโมเดลอย่างไรเมื่อเฟรมเวิร์ก automation ปรับตัว