├── Makefile ├── README.md ├── apply-evasions.js ├── main.js └── tests ├── adscore.html └── distil.html /Makefile: -------------------------------------------------------------------------------- 1 | MAIN_JS_FILE := main.js 2 | 3 | all: 4 | # format everything 5 | npx prettier --single-quote false --html-whitespace-sensitivity strict --no-bracket-spacing --quote-props preserve --trailing-comma all --write --tab-width 4 *.js tests/*.html 6 | # clear terminal 7 | reset 8 | # run browser 9 | node "${MAIN_JS_FILE}" 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## puppeteer-bypassing-bot-detection 2 | 3 | This repository is incomplete and not actively maintained. It was primarily a test to see the minimum amount needed to bypass Distil networks and other public headless checks. Use https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/. 4 | 5 | This repository contains code for bypassing common bot detection checks by a few vendors ([Distil Networks](https://www.whitepages.com/dstl-wp.js), [Adscore](http://c.adsco.re/d), and [Google IMA](https://imasdk.googleapis.com/js/sdkloader/ima3.js)). 6 | 7 | Patched attributes 8 | 9 | * navigator.webdriver 10 | * navigator.permissions.query 11 | * document.hasFocus 12 | * document.hidden 13 | * document.visiblityState 14 | * document.onvisiblitychange 15 | * navigator.mimeTypes 16 | * navigator.plugins 17 | * navigator.languages 18 | * screen.width / screen.height 19 | * screen.availWidth / screen.availHeight 20 | * window.innerWidth / window.innerHeight 21 | * window.outerWidth / window.outerHeight 22 | * document.documentElement.clientWidth / document.documentElement.clientHeight 23 | * window.matchMedia 24 | * AudioElement.canPlayType 25 | * VideoElement.canPlayType 26 | * screen.orientation.type 27 | * screen.orientation.angle 28 | * Fix toString of HTMLElement.prototype.animate (Puppeteer doesn't make it "native code") 29 | * Fake WebGLRenderingContext.prototype.getParameter values 30 | * Make iframes have same window attribute 31 | * Make window.alert a stub 32 | * HTMLImageElement.prototype.width / HTMLImageElement.prototype.height for broken images 33 | * Undetectable toString patched with Proxy 34 | * window.chrome 35 | * csi 36 | * loadTimes 37 | * app 38 | * isInstalled 39 | * getDetails 40 | * getIsInstalled 41 | * runningState 42 | * InstallState 43 | * RunningState 44 | -------------------------------------------------------------------------------- /apply-evasions.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (page) { 2 | /* 3 | TODO: Add windows profile. Sophisticated bot detection vendors check TCP stack features (see p0f) so system level changes may be necessary. 4 | Will likely need to add: 5 | - navigator.platform 6 | - Different vendor/renderer names (DirectX vs Mesa) for WebGL getParameter 7 | - Different codec support (untested) 8 | - Different avail/inner/outer widths/heights because Windows has a different size taskbar and other things 9 | - Widevine plugin 10 | */ 11 | 12 | const userAgent = 13 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36"; 14 | 15 | await page.setUserAgent(userAgent); 16 | 17 | // pass the Webdriver test 18 | await page.evaluateOnNewDocument(() => { 19 | delete navigator.webdriver; 20 | delete Navigator.prototype.webdriver; 21 | }); 22 | 23 | // pass the permissions test by denying all permissions 24 | await page.evaluateOnNewDocument(() => { 25 | const originalQuery = window.navigator.permissions.query; 26 | Permissions.prototype.query = function query(parameters) { 27 | if (!parameters || !parameters.name) 28 | return originalQuery(parameters); 29 | 30 | return Promise.resolve({ 31 | state: "denied", 32 | onchange: null, 33 | }); 34 | }; 35 | }); 36 | 37 | // Fake standard visiblity checks 38 | await page.evaluateOnNewDocument(() => { 39 | // https://adtechmadness.wordpress.com/2019/03/14/spoofing-viewability-measurements-technical-examples/ 40 | Object.defineProperty(Document.prototype, "hasFocus", { 41 | value: function hasFocus(document) { 42 | return true; 43 | }, 44 | }); 45 | Object.defineProperty(Document.prototype, "hidden", { 46 | get: () => false, 47 | }); 48 | Object.defineProperty(Document.prototype, "visiblityState", { 49 | get: () => "visible", 50 | }); 51 | // window.locationbar.visible, window.menubar.visible, window.personalbar.visible, window.scrollbars.visible, window.statusbar.visible, window.toolbar.visible 52 | Object.defineProperty(BarProp.prototype, "visible", { 53 | get: () => true, 54 | }); 55 | Object.defineProperty(Document.prototype, "onvisiblitychange", { 56 | set: (params) => function () {}, // ignore visiblity changes even when an event handler is registered 57 | }); 58 | }); 59 | 60 | // Set plugins to Chrome's 61 | await page.evaluateOnNewDocument(() => { 62 | /* global MimeType MimeTypeArray PluginArray */ 63 | 64 | const fakeData = { 65 | mimeTypes: [ 66 | { 67 | type: "application/pdf", 68 | suffixes: "pdf", 69 | description: "", 70 | __pluginName: "Chrome PDF Viewer", 71 | }, 72 | { 73 | type: "application/x-google-chrome-pdf", 74 | suffixes: "pdf", 75 | description: "Portable Document Format", 76 | __pluginName: "Chrome PDF Plugin", 77 | }, 78 | { 79 | type: "application/x-nacl", 80 | suffixes: "", 81 | description: "Native Client Executable", 82 | enabledPlugin: Plugin, 83 | __pluginName: "Native Client", 84 | }, 85 | { 86 | type: "application/x-pnacl", 87 | suffixes: "", 88 | description: "Portable Native Client Executable", 89 | __pluginName: "Native Client", 90 | }, 91 | ], 92 | plugins: [ 93 | { 94 | name: "Chrome PDF Plugin", 95 | filename: "internal-pdf-viewer", 96 | description: "Portable Document Format", 97 | }, 98 | { 99 | name: "Chrome PDF Viewer", 100 | filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", 101 | description: "", 102 | }, 103 | { 104 | name: "Native Client", 105 | filename: "internal-nacl-plugin", 106 | description: "", 107 | }, 108 | ], 109 | fns: { 110 | namedItem: (instanceName) => { 111 | // Returns the Plugin/MimeType with the specified name. 112 | return function namedItem(name) { 113 | if (!arguments.length) { 114 | throw new TypeError( 115 | `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`, 116 | ); 117 | } 118 | return this[name] || null; 119 | }; 120 | }, 121 | item: (instanceName) => { 122 | // Returns the Plugin/MimeType at the specified index into the array. 123 | return function item(index) { 124 | if (!arguments.length) { 125 | throw new TypeError( 126 | `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`, 127 | ); 128 | } 129 | return this[index] || null; 130 | }; 131 | }, 132 | refresh: (instanceName) => { 133 | // Refreshes all plugins on the current page, optionally reloading documents. 134 | return function refresh() { 135 | return undefined; 136 | }; 137 | }, 138 | }, 139 | }; 140 | // Poor mans _.pluck 141 | const getSubset = (keys, obj) => 142 | keys.reduce((a, c) => ({...a, [c]: obj[c]}), {}); 143 | 144 | function generateMimeTypeArray() { 145 | const arr = fakeData.mimeTypes 146 | .map((obj) => 147 | getSubset(["type", "suffixes", "description"], obj), 148 | ) 149 | .map((obj) => Object.setPrototypeOf(obj, MimeType.prototype)); 150 | arr.forEach((obj) => { 151 | Object.defineProperty(arr, obj.type, { 152 | value: obj, 153 | enumerable: false, // make sure its not enumerable or distil networks will put duplicates in their list 154 | }); 155 | }); 156 | 157 | // Mock functions 158 | arr.namedItem = fakeData.fns.namedItem("MimeTypeArray"); 159 | arr.item = fakeData.fns.item("MimeTypeArray"); 160 | 161 | return Object.setPrototypeOf(arr, MimeTypeArray.prototype); 162 | } 163 | 164 | const mimeTypeArray = generateMimeTypeArray(); 165 | Object.defineProperty(Object.getPrototypeOf(navigator), "mimeTypes", { 166 | get: () => mimeTypeArray, 167 | }); 168 | 169 | function generatePluginArray() { 170 | const arr = fakeData.plugins 171 | .map((obj) => 172 | getSubset(["name", "filename", "description"], obj), 173 | ) 174 | .map((obj) => { 175 | const mimes = fakeData.mimeTypes.filter( 176 | (m) => m.__pluginName === obj.name, 177 | ); 178 | // Add mimetypes 179 | mimes.forEach((mime, index) => { 180 | navigator.mimeTypes[mime.type].enabledPlugin = obj; 181 | obj[mime.type] = navigator.mimeTypes[mime.type]; 182 | obj[index] = navigator.mimeTypes[mime.type]; 183 | }); 184 | obj.length = mimes.length; 185 | return obj; 186 | }) 187 | .map((obj) => { 188 | // Mock functions 189 | obj.namedItem = fakeData.fns.namedItem("Plugin"); 190 | obj.item = fakeData.fns.item("Plugin"); 191 | return obj; 192 | }) 193 | .map((obj) => Object.setPrototypeOf(obj, Plugin.prototype)); 194 | arr.forEach((obj) => { 195 | Object.defineProperty(arr, obj.name, { 196 | value: obj, 197 | enumerable: false, // make sure its not enumerable or distil networks will put duplicates in their list 198 | }); 199 | }); 200 | 201 | // Mock functions 202 | arr.namedItem = fakeData.fns.namedItem("PluginArray"); 203 | arr.item = fakeData.fns.item("PluginArray"); 204 | arr.refresh = fakeData.fns.refresh("PluginArray"); 205 | 206 | return Object.setPrototypeOf(arr, PluginArray.prototype); 207 | } 208 | 209 | const pluginArray = generatePluginArray(); 210 | Object.defineProperty(Object.getPrototypeOf(navigator), "plugins", { 211 | get: () => pluginArray, 212 | }); 213 | }); 214 | 215 | // Puppeteer defines languages to be "" for some reason 216 | await page.evaluateOnNewDocument(() => { 217 | Object.defineProperty(Object.getPrototypeOf(navigator), "languages", { 218 | get: () => ["en-US", "en"], 219 | }); 220 | }); 221 | 222 | // Fake resolution info 223 | await page.evaluateOnNewDocument(() => { 224 | const resolution = { 225 | width: 1366, 226 | height: 768, 227 | }; 228 | Object.defineProperty(Screen.prototype, "width", { 229 | get: () => resolution.width, 230 | }); 231 | Object.defineProperty(Screen.prototype, "height", { 232 | get: () => resolution.height, 233 | }); 234 | Object.defineProperty(Screen.prototype, "availWidth", { 235 | get: () => resolution.width, 236 | }); 237 | Object.defineProperty(Screen.prototype, "availHeight", { 238 | get: () => resolution.height, 239 | }); 240 | 241 | Object.defineProperty(window, "innerWidth", { 242 | get: () => resolution.width, 243 | }); 244 | Object.defineProperty(window, "innerHeight", { 245 | get: () => resolution.height - 72, 246 | }); 247 | Object.defineProperty(window, "outerWidth", { 248 | get: () => resolution.width, 249 | }); 250 | Object.defineProperty(window, "outerHeight", { 251 | get: () => resolution.height, 252 | }); 253 | Object.defineProperty(HTMLHtmlElement.prototype, "clientWidth", { 254 | get: () => window.innerWidth, 255 | }); 256 | Object.defineProperty(HTMLHtmlElement.prototype, "clientHeight", { 257 | get: () => window.innerHeight, 258 | }); 259 | 260 | // Fake min-width based resolution checks 261 | const originalMatchMedia = window.matchMedia; 262 | Object.defineProperty(window, "matchMedia", { 263 | value: function matchMedia(query) { 264 | var lowerQuery = query.toLowerCase(); 265 | var result = originalMatchMedia(query); 266 | if (lowerQuery.includes("min-width")) { 267 | Object.defineProperty(result, "matches", { 268 | get: () => true, 269 | }); 270 | } 271 | 272 | return result; 273 | }, 274 | }); 275 | }); 276 | 277 | // Codec support 278 | await page.evaluateOnNewDocument(() => { 279 | // ACCEPTED CODECS UNUSED 280 | const acceptedCodecs = [ 281 | "audio/ogg;codecs=flac", 282 | "audio/ogg;codecs=vorbis", 283 | 'audio/mp4; codecs="mp4a.40.2"', 284 | 'audio/mpeg;codecs="mp3"', 285 | 'video/mp4; codecs="avc1.42E01E"', 286 | 'video/mp4;codecs="avc1.42E01E, mp4a.40.2"', 287 | 'video/mp4;codecs="avc1.4D401E, mp4a.40.2"', 288 | 'video/mp4;codecs="avc1.58A01E, mp4a.40.2"', 289 | 'video/mp4;codecs="avc1.64001E, mp4a.40.2"', 290 | 'video/ogg;codecs="theora, vorbis"', 291 | 'video/webm; codecs="vorbis,vp8"', 292 | ]; 293 | 294 | Object.defineProperty(HTMLVideoElement.prototype, "canPlayType", { 295 | value: function canPlayType(codec) { 296 | codec = codec.toLowerCase(); 297 | if ( 298 | codec.includes("ogg") || 299 | codec.includes("mp4") || 300 | codec.includes("webm") || 301 | codec.includes("mp3") || 302 | codec.includes("mpeg") || 303 | codec.includes("wav") 304 | ) { 305 | return "probably"; 306 | } else if (codec.includes("wav")) { 307 | return "maybe"; 308 | } else { 309 | return ""; 310 | } 311 | }, 312 | }); 313 | Object.defineProperty(HTMLAudioElement.prototype, "canPlayType", { 314 | value: function canPlayType(codec) { 315 | codec = codec.toLowerCase(); 316 | if ( 317 | codec.includes("ogg") || 318 | codec.includes("mp4") || 319 | codec.includes("webm") || 320 | codec.includes("mp3") || 321 | codec.includes("mpeg") || 322 | codec.includes("wav") 323 | ) { 324 | return "probably"; 325 | } else if (codec.includes("m4a")) { 326 | return "maybe"; 327 | } else { 328 | return ""; 329 | } 330 | }, 331 | }); 332 | }); 333 | 334 | // Standard desktop screen orientation 335 | await page.evaluateOnNewDocument(() => { 336 | Object.defineProperty(ScreenOrientation.prototype, "type", { 337 | get: () => "landscape-primary", 338 | }); 339 | Object.defineProperty(ScreenOrientation.prototype, "angle", { 340 | get: () => 0, 341 | }); 342 | }); 343 | 344 | // Fix HTMLElement animate toString (Puppeteer doesn't make it native code for some reason) 345 | await page.evaluateOnNewDocument(() => { 346 | const oldAnimate = HTMLElement.prototype.animate; 347 | Object.defineProperty(HTMLElement.prototype, "animate", { 348 | value: function animate(parameters) { 349 | return oldAnimate(this, parameters); 350 | }, 351 | }); 352 | }); 353 | await page.evaluateOnNewDocument(() => { 354 | WebGLRenderingContext.prototype.getParameter = (function getParameter( 355 | originalFunction, 356 | ) { 357 | // TODO: Remove linux strings like Mesa and OpenGL and find Windows version 358 | const paramMap = {}; 359 | // UNMASKED_VENDOR_WEBGL 360 | paramMap[0x9245] = "Intel Open Source Technology Center"; 361 | // UNMASKED_RENDERER_WEBGL 362 | paramMap[0x9246] = 363 | "Mesa DRI Intel(R) HD Graphics 5500 (Broadwell GT2)"; 364 | // VENDOR 365 | paramMap[0x1f00] = "WebKit"; 366 | // RENDERER 367 | paramMap[0x1f01] = "WebKit WebGL"; 368 | // VERSION 369 | paramMap[0x1f02] = "WebGL 1.0 (OpenGL ES 2.0 Chromium)"; 370 | 371 | return function getParameter(parameter) { 372 | return ( 373 | paramMap[parameter] || 374 | originalFunction.call(this, parameter) 375 | ); 376 | }; 377 | })(WebGLRenderingContext.prototype.getParameter); 378 | }); 379 | 380 | // Overwrite iframe window object so we don't have to reapply the above evasions for every iframe 381 | // Stolen from https://github.com/berstend/puppeteer-extra/blob/ceca9c6fed0a9f39d6c80b71fd413f3656ebb704/packages/puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow/index.js 382 | await page.evaluateOnNewDocument(() => { 383 | try { 384 | // Adds a contentWindow proxy to the provided iframe element 385 | const addContentWindowProxy = (iframe) => { 386 | const contentWindowProxy = { 387 | get(target, key) { 388 | // Now to the interesting part: 389 | // We actually make this thing behave like a regular iframe window, 390 | // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :) 391 | // That makes it possible for these assertions to be correct: 392 | // iframe.contentWindow.self === window.top // must be false 393 | if (key === "self") { 394 | return this; 395 | } 396 | // iframe.contentWindow.frameElement === iframe // must be true 397 | if (key === "frameElement") { 398 | return iframe; 399 | } 400 | return Reflect.get(target, key); 401 | }, 402 | }; 403 | 404 | if (!iframe.contentWindow) { 405 | const proxy = new Proxy(window, contentWindowProxy); 406 | Object.defineProperty(iframe, "contentWindow", { 407 | get() { 408 | return proxy; 409 | }, 410 | set(newValue) { 411 | return newValue; // contentWindow is immutable 412 | }, 413 | enumerable: true, 414 | configurable: false, 415 | }); 416 | } 417 | }; 418 | 419 | // Handles iframe element creation, augments `srcdoc` property so we can intercept further 420 | const handleIframeCreation = (target, thisArg, args) => { 421 | const iframe = target.apply(thisArg, args); 422 | 423 | // We need to keep the originals around 424 | const _iframe = iframe; 425 | const _srcdoc = _iframe.srcdoc; 426 | 427 | // Add hook for the srcdoc property 428 | // We need to be very surgical here to not break other iframes by accident 429 | Object.defineProperty(iframe, "srcdoc", { 430 | configurable: true, // Important, so we can reset this later 431 | get: function () { 432 | return _iframe.srcdoc; 433 | }, 434 | set: function (newValue) { 435 | addContentWindowProxy(this); 436 | // Reset property, the hook is only needed once 437 | Object.defineProperty(iframe, "srcdoc", { 438 | configurable: false, 439 | writable: false, 440 | value: _srcdoc, 441 | }); 442 | _iframe.srcdoc = newValue; 443 | }, 444 | }); 445 | return iframe; 446 | }; 447 | 448 | // Adds a hook to intercept iframe creation events 449 | const addIframeCreationSniffer = () => { 450 | /* global document */ 451 | const createElement = { 452 | // Make toString() native 453 | get(target, key) { 454 | return Reflect.get(target, key); 455 | }, 456 | apply: function (target, thisArg, args) { 457 | const isIframe = 458 | args && 459 | args.length && 460 | `${args[0]}`.toLowerCase() === "iframe"; 461 | if (!isIframe) { 462 | // Everything as usual 463 | return target.apply(thisArg, args); 464 | } else { 465 | return handleIframeCreation(target, thisArg, args); 466 | } 467 | }, 468 | }; 469 | // All this just due to iframes with srcdoc bug 470 | document.createElement = new Proxy( 471 | document.createElement, 472 | createElement, 473 | ); 474 | }; 475 | 476 | // Let's go 477 | addIframeCreationSniffer(); 478 | } catch (err) {} 479 | }); 480 | 481 | // disable alert since it blocks 482 | await page.evaluateOnNewDocument(() => { 483 | Object.defineProperty(window, "alert", { 484 | value: function alert(parameter) { 485 | return undefined; 486 | }, 487 | }); 488 | }); 489 | 490 | // default broken image test 491 | await page.evaluateOnNewDocument(() => { 492 | ["height", "width"].forEach((property) => { 493 | // store the existing descriptor 494 | const imageDescriptor = Object.getOwnPropertyDescriptor( 495 | HTMLImageElement.prototype, 496 | property, 497 | ); 498 | 499 | // redefine the property with a patched descriptor 500 | Object.defineProperty(HTMLImageElement.prototype, property, { 501 | ...imageDescriptor, 502 | get: function () { 503 | // return an arbitrary non-zero dimension if the image failed to load 504 | if (this.complete && this.naturalHeight == 0) { 505 | return 16; 506 | } 507 | // otherwise, return the actual dimension 508 | return imageDescriptor.get.apply(this); 509 | }, 510 | }); 511 | }); 512 | }); 513 | 514 | await page.evaluateOnNewDocument(() => { 515 | /* Copied from Google Chrome v83 on Linux */ 516 | var currentTime = new Date().getTime(); 517 | var currentTimeDivided = currentTime / 1000; 518 | var randOffset = Math.random() * 3; 519 | 520 | Object.defineProperty(window, "chrome", { 521 | writable: true, 522 | enumerable: true, 523 | configurable: false, // note! 524 | value: {}, // We'll extend that later 525 | }); 526 | 527 | Object.defineProperty(window.chrome, "csi", { 528 | value: function csi() { 529 | /* https://chromium.googlesource.com/chromium/src.git/+/master/chrome/renderer/loadtimes_extension_bindings.cc */ 530 | return { 531 | startE: currentTime, 532 | onloadT: currentTime + 3 * randOffset, 533 | pageT: 30000 * randOffset, 534 | tran: 15, 535 | }; 536 | }, 537 | }); 538 | 539 | Object.defineProperty(window.chrome, "loadTimes", { 540 | value: function loadTimes() { 541 | return { 542 | requestTime: currentTimeDivided + 1 * randOffset, 543 | startLoadTime: currentTimeDivided + 1 * randOffset, 544 | commitLoadTme: currentTimeDivided + 2 * randOffset, 545 | finishDocumentLoadTime: currentTimeDivided + 3 * randOffset, 546 | firstPaintTime: currentTimeDivided + 4 * randOffset, 547 | finishLoadTime: currentTimeDivided + 5 * randOffset, 548 | firstPaintAfterLoadTime: 0, 549 | navigationType: "Other", 550 | wasFetchedViaSpdy: true, 551 | wasNpnNegotiated: true, 552 | npnNegotiatedProtocol: "h2", 553 | wasAlternateProtocolAvailable: false, 554 | connectionInfo: "h2", 555 | }; 556 | }, 557 | }); 558 | 559 | const stripErrorWithAnchor = (err, anchor) => { 560 | const stackArr = err.stack.split("\n"); 561 | const anchorIndex = stackArr.findIndex((line) => 562 | line.trim().startsWith(anchor), 563 | ); 564 | if (anchorIndex === -1) { 565 | return err; // 404, anchor not found 566 | } 567 | // Strip everything from the top until we reach the anchor line (remove anchor line as well) 568 | // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) 569 | stackArr.splice(1, anchorIndex); 570 | err.stack = stackArr.join("\n"); 571 | return err; 572 | }; 573 | 574 | const makeError = { 575 | ErrorInInvocation: (fn) => { 576 | const err = new TypeError(`Error in invocation of app.${fn}()`); 577 | return stripErrorWithAnchor( 578 | err, 579 | `at ${fn} (eval at `, 580 | ); 581 | }, 582 | }; 583 | 584 | // https://github.com/berstend/puppeteer-extra/blob/9c3d4aace43cb44da984f1e2f581ad376ebefeea/packages/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js 585 | Object.defineProperty(window.chrome, "app", { 586 | value: { 587 | InstallState: { 588 | DISABLED: "disabled", 589 | INSTALLED: "installed", 590 | NOT_INSTALLED: "not_installed", 591 | }, 592 | RunningState: { 593 | CANNOT_RUN: "cannot_run", 594 | READY_TO_RUN: "ready_to_run", 595 | RUNNING: "running", 596 | }, 597 | get isInstalled() { 598 | return false; 599 | }, 600 | getDetails: function getDetails() { 601 | if (arguments.length) { 602 | throw makeError.ErrorInInvocation(`getDetails`); 603 | } 604 | return null; 605 | }, 606 | getIsInstalled: function getIsInstalled() { 607 | if (arguments.length) { 608 | throw makeError.ErrorInInvocation(`getIsInstalled`); 609 | } 610 | return false; 611 | }, 612 | runningState: function runningState() { 613 | if (arguments.length) { 614 | throw makeError.ErrorInInvocation(`runningState`); 615 | } 616 | return "cannot_run"; 617 | }, 618 | }, 619 | }); 620 | }); 621 | 622 | /* Evade toString detection */ 623 | await page.evaluateOnNewDocument(() => { 624 | // Spoofs the toString output of the following functions to native code. If you spoof another function, add it to this list. 625 | var functionList = [ 626 | Permissions.prototype.query, 627 | window.alert, 628 | Document.prototype.hasFocus, 629 | WebGLRenderingContext.prototype.getParameter, 630 | navigator.mimeTypes.item, 631 | navigator.mimeTypes.namedItem, 632 | navigator.plugins.refresh, 633 | HTMLVideoElement.prototype.canPlayType, 634 | HTMLAudioElement.prototype.canPlayType, 635 | window.matchMedia, 636 | Object.getOwnPropertyDescriptor(Screen.prototype, "height").get, 637 | Object.getOwnPropertyDescriptor(Screen.prototype, "width").get, 638 | Object.getOwnPropertyDescriptor(Screen.prototype, "availHeight") 639 | .get, 640 | Object.getOwnPropertyDescriptor(ScreenOrientation.prototype, "type") 641 | .get, 642 | Object.getOwnPropertyDescriptor( 643 | ScreenOrientation.prototype, 644 | "angle", 645 | ).get, 646 | Object.getOwnPropertyDescriptor(Screen.prototype, "availWidth").get, 647 | Object.getOwnPropertyDescriptor(Document.prototype, "hidden").get, 648 | Object.getOwnPropertyDescriptor( 649 | Document.prototype, 650 | "visiblityState", 651 | ).get, 652 | Object.getOwnPropertyDescriptor(BarProp.prototype, "visible").get, 653 | Object.getOwnPropertyDescriptor(Navigator.prototype, "mimeTypes") 654 | .get, 655 | Object.getOwnPropertyDescriptor(Navigator.prototype, "plugins").get, 656 | Object.getOwnPropertyDescriptor(Navigator.prototype, "languages") 657 | .get, 658 | Object.getOwnPropertyDescriptor(window, "innerWidth").get, 659 | Object.getOwnPropertyDescriptor(window, "innerHeight").get, 660 | Object.getOwnPropertyDescriptor(window, "outerWidth").get, 661 | Object.getOwnPropertyDescriptor(window, "outerHeight").get, 662 | Object.getOwnPropertyDescriptor( 663 | HTMLHtmlElement.prototype, 664 | "clientWidth", 665 | ).get, 666 | Object.getOwnPropertyDescriptor( 667 | HTMLHtmlElement.prototype, 668 | "clientHeight", 669 | ).get, 670 | Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "width") 671 | .get, 672 | Object.getOwnPropertyDescriptor( 673 | HTMLImageElement.prototype, 674 | "height", 675 | ).get, 676 | HTMLElement.prototype.animate, 677 | window.chrome.csi, 678 | window.chrome.loadTimes, 679 | window.chrome.app.getDetails, 680 | window.chrome.app.getIsInstalled, 681 | window.chrome.app.runningState, 682 | Object.getOwnPropertyDescriptor(window.chrome.app, "isInstalled") 683 | .get, 684 | document.createElement, 685 | ]; 686 | 687 | // Undetecable toString modification - https://adtechmadness.wordpress.com/2019/03/23/javascript-tampering-detection-and-stealth/ */ 688 | var toStringProxy = new Proxy(Function.prototype.toString, { 689 | apply: function toString(target, thisArg, args) { 690 | // Special functions we make always return "native code" 691 | // NOTE: This depends on the functions being named (see hasFocus example). Anonymous functions will not work (or at least will not show the proper output) because their name attribute is equal to "". 692 | if (functionList.includes(thisArg)) { 693 | return "function " + thisArg.name + "() { [native code] }"; 694 | } else { 695 | return target.call(thisArg); 696 | } 697 | }, 698 | }); 699 | 700 | Function.prototype.toString = toStringProxy; 701 | functionList.push(Function.prototype.toString); // now that its modified, we can add it 702 | }); 703 | }; 704 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const applyEvasions = require("./apply-evasions"); 3 | 4 | (async () => { 5 | const browser = await puppeteer.launch({ 6 | args: [ 7 | "--no-sandbox", 8 | "--disable-setuid-sandbox", 9 | "--disable-web-security", 10 | "--disable-setuid-sandbox", 11 | "--disable-infobars", 12 | "--window-position=0,0", 13 | "--lang=en-US,en;q=0.9", // send accept-language header 14 | "--proxy-server=154.13.49.12:22222", // hola us cogent server 15 | ], 16 | // headless: false, 17 | headless: true, 18 | ignoreHTTPSErrors: true, 19 | dumpio: true, // log console events 20 | }); 21 | 22 | const page = await browser.newPage(); 23 | 24 | // proxy credentials 25 | await page.authenticate({ 26 | username: "user-uuid-63add07fed46a05cdefc96332437cbbf", 27 | password: "8c212c805e06", 28 | }); 29 | 30 | await applyEvasions(page); 31 | 32 | /* PERIMETERX SITES */ 33 | // await page.goto("https://www.usa-people-search.com/names/a_400_599_1", { 34 | // waitUntil: "networkidle0", 35 | // }); 36 | 37 | /* DISTIL SITES (PASSED) */ 38 | await page.goto("https://lufthansa.com", { 39 | waitUntil: "networkidle0", 40 | }); 41 | // await page.goto("https://streeteasy.com", { 42 | // waitUntil: "networkidle0", 43 | // }); 44 | 45 | /* RECAPTCHA SCORE PAGE */ 46 | // await page.goto( 47 | // "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", 48 | // ); 49 | 50 | /* 51 | LOCAL TESTING SITES (see tests folder) 52 | To test these, I ran these on a real browser and the headless browser. I made fixes in the headless browser until the outputs matched. 53 | */ 54 | // await page.goto("http://localhost:8000/distil.html"); 55 | // await page.goto("http://localhost:8000/adscore.html"); 56 | 57 | /* OTHER TESTING SITES (PASSED) */ 58 | // await page.goto("https://antoinevastel.com/bots"); 59 | // await page.goto("https://infosimples.github.io/detect-headless/"); 60 | // await page.goto("https://arh.antoinevastel.com/bots/areyouheadless"); 61 | 62 | // if true we passed distil check 63 | console.log( 64 | (await page.title()) === 65 | "Book a flight now and discover the world | Lufthansa", 66 | ); 67 | 68 | await browser.close(); 69 | })(); 70 | -------------------------------------------------------------------------------- /tests/adscore.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demonstration 6 | 7 | 8 | 415 | 416 | 417 | -------------------------------------------------------------------------------- /tests/distil.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demonstration 6 | 7 | 8 |
    9 | 738 | 739 | 740 | --------------------------------------------------------------------------------