UNPKG

puppeteer-extra-plugin-stealth

Version:

Stealth mode: Applies various techniques to make detection of headless puppeteer harder.

395 lines (322 loc) 12.1 kB
const test = require('ava') const { getVanillaFingerPrint, getStealthFingerPrint, dummyHTMLPath, vanillaPuppeteer, addExtra } = require('../../test/util') // const Plugin = require('.') // NOTE: We're using the full plugin for testing here as `iframe.contentWindow` uses data set by `chrome.runtime` const Plugin = require('puppeteer-extra-plugin-stealth') // Fix CI issues with old versions const isOldPuppeteerVersion = () => { const version = process.env.PUPPETEER_VERSION const isOld = version && (version === '1.9.0' || version === '1.6.2') return isOld } test('vanilla: will be undefined', async t => { const { iframeChrome } = await getVanillaFingerPrint() t.is(iframeChrome, 'undefined') }) test('stealth: will be object', async t => { const { iframeChrome } = await getStealthFingerPrint(Plugin) t.is(iframeChrome, 'object') }) test('stealth: will not break iframes', async t => { const browser = await addExtra(vanillaPuppeteer) .use(Plugin()) .launch({ headless: true }) const page = await browser.newPage() const testFuncReturnValue = 'TESTSTRING' await page.evaluate(returnValue => { const { document } = window // eslint-disable-line const body = document.querySelector('body') const iframe = document.createElement('iframe') iframe.srcdoc = 'foobar' iframe.contentWindow.mySuperFunction = () => returnValue body.appendChild(iframe) }, testFuncReturnValue) const realReturn = await page.evaluate( () => document.querySelector('iframe').contentWindow.mySuperFunction() // eslint-disable-line ) await browser.close() t.is(realReturn, 'TESTSTRING') }) test('vanilla: will not have contentWindow[0]', async t => { const browser = await vanillaPuppeteer.launch({ headless: true }) const page = await browser.newPage() const zero = await page.evaluate(returnValue => { const { document } = window // eslint-disable-line const body = document.querySelector('body') const iframe = document.createElement('iframe') iframe.srcdoc = 'foobar' body.appendChild(iframe) return typeof iframe.contentWindow[0] }) await browser.close() t.is(zero, 'undefined') }) test('stealth: will not have contentWindow[0]', async t => { const browser = await addExtra(vanillaPuppeteer) .use(Plugin()) .launch({ headless: true }) const page = await browser.newPage() const zero = await page.evaluate(returnValue => { const { document } = window // eslint-disable-line const body = document.querySelector('body') const iframe = document.createElement('iframe') iframe.srcdoc = 'foobar' body.appendChild(iframe) return typeof iframe.contentWindow[0] }) await browser.close() t.is(zero, 'undefined') }) test('vanilla: will not have chrome runtine in any frame', async t => { const browser = await vanillaPuppeteer.launch({ headless: true }) const page = await browser.newPage() await page.goto('file://' + dummyHTMLPath) const basiciframe = await page.evaluate(() => { const el = document.createElement('iframe') document.body.appendChild(el) return el.contentWindow.chrome }) const sandboxSOiframe = await page.evaluate(() => { const el = document.createElement('iframe') el.setAttribute('sandbox', 'allow-same-origin') document.body.appendChild(el) return el.contentWindow.chrome }) const sandboxSOASiframe = await page.evaluate(() => { const el = document.createElement('iframe') el.setAttribute('sandbox', 'allow-same-origin allow-scripts') document.body.appendChild(el) return el.contentWindow.chrome }) const srcdociframe = await page.evaluate(() => { const el = document.createElement('iframe') el.srcdoc = 'blank page, boys.' document.body.appendChild(el) return el.contentWindow.chrome }) // console.log('basic iframe', basiciframe) // console.log('sandbox same-origin iframe', sandboxSOiframe) // console.log('sandbox same-origin&scripts iframe', sandboxSOASiframe) // console.log('srcdoc iframe', srcdociframe) await browser.close() t.is(typeof basiciframe, 'undefined') t.is(typeof sandboxSOiframe, 'undefined') t.is(typeof sandboxSOASiframe, 'undefined') t.is(typeof srcdociframe, 'undefined') }) test('vanilla: will return empty srcdoc by default', async t => { const browser = await vanillaPuppeteer.launch({ headless: true }) const page = await browser.newPage() const srcdoc = await page.evaluate(returnValue => { const { document } = window // eslint-disable-line const iframe = document.createElement('iframe') return iframe.srcdoc }) await browser.close() t.is(srcdoc, '') }) test('stealth: will return empty srcdoc by default', async t => { const browser = await addExtra(vanillaPuppeteer) .use(Plugin()) .launch({ headless: true }) const page = await browser.newPage() const srcdoc = await page.evaluate(returnValue => { const { document } = window // eslint-disable-line const iframe = document.createElement('iframe') return iframe.srcdoc }) await browser.close() t.is(srcdoc, '') }) test('stealth: it will cover all frames including srcdoc', async t => { // const browser = await vanillaPuppeteer.launch({ headless: false }) const browser = await addExtra(vanillaPuppeteer) .use(Plugin()) .launch({ headless: true }) const page = await browser.newPage() await page.goto('file://' + dummyHTMLPath) const basiciframe = await page.evaluate(() => { const el = document.createElement('iframe') document.body.appendChild(el) return el.contentWindow.chrome }) const sandboxSOiframe = await page.evaluate(() => { const el = document.createElement('iframe') el.setAttribute('sandbox', 'allow-same-origin') document.body.appendChild(el) return el.contentWindow.chrome }) const sandboxSOASiframe = await page.evaluate(() => { const el = document.createElement('iframe') el.setAttribute('sandbox', 'allow-same-origin allow-scripts') document.body.appendChild(el) return el.contentWindow.chrome }) const srcdociframe = await page.evaluate(() => { const el = document.createElement('iframe') el.srcdoc = 'blank page, boys.' document.body.appendChild(el) return el.contentWindow.chrome }) // console.log('basic iframe', basiciframe) // console.log('sandbox same-origin iframe', sandboxSOiframe) // console.log('sandbox same-origin&scripts iframe', sandboxSOASiframe) // console.log('srcdoc iframe', srcdociframe) await browser.close() if (isOldPuppeteerVersion()) { t.is(typeof basiciframe, 'object') } else { t.is(typeof basiciframe, 'object') t.is(typeof sandboxSOiframe, 'object') t.is(typeof sandboxSOASiframe, 'object') t.is(typeof srcdociframe, 'object') } }) /* global HTMLIFrameElement */ test('stealth: it will emulate advanved contentWindow features correctly', async t => { // const browser = await vanillaPuppeteer.launch({ headless: false }) const browser = await addExtra(vanillaPuppeteer) .use(Plugin()) .launch({ headless: true }) const page = await browser.newPage() await page.goto('file://' + dummyHTMLPath) // page.on('console', msg => { // console.log('Page console: ', msg.text()) // }) const results = await page.evaluate(() => { const results = {} const iframe = document.createElement('iframe') iframe.srcdoc = 'page intentionally left blank' // Note: srcdoc document.body.appendChild(iframe) const basicIframe = document.createElement('iframe') basicIframe.src = 'data:text/plain;charset=utf-8,foobar' document.body.appendChild(iframe) results.descriptorsOK = (() => { // Verify iframe prototype isn't touched const descriptors = Object.getOwnPropertyDescriptors( HTMLIFrameElement.prototype ) const str = descriptors.contentWindow.get.toString() return str === `function get contentWindow() { [native code] }` })() results.noProxySignature = (() => { return iframe.srcdoc.toString.hasOwnProperty('[[IsRevoked]]') // eslint-disable-line })() results.doesExist = (() => { // Verify iframe isn't remapped to main window return !!iframe.contentWindow })() results.isNotAClone = (() => { // Verify iframe isn't remapped to main window return iframe.contentWindow !== window })() results.hasPlugins = (() => { return iframe.contentWindow.navigator.plugins.length > 0 })() results.hasSameNumberOfPlugins = (() => { return ( window.navigator.plugins.length === iframe.contentWindow.navigator.plugins.length ) })() results.SelfIsNotWindow = (() => { return iframe.contentWindow.self !== window })() results.SelfIsNotWindowTop = (() => { return iframe.contentWindow.self !== window.top })() results.TopIsNotSame = (() => { return iframe.contentWindow.top !== iframe.contentWindow })() results.FrameElementMatches = (() => { return iframe.contentWindow.frameElement === iframe })() results.StackTraces = (() => { try { // eslint-disable-next-line document['createElement'](0) } catch (e) { return e.stack } return false })() return results }) await browser.close() if (isOldPuppeteerVersion()) { t.true(true) return } t.true(results.descriptorsOK) t.true(results.doesExist) t.true(results.isNotAClone) t.true(results.hasPlugins) t.true(results.hasSameNumberOfPlugins) t.true(results.SelfIsNotWindow) t.true(results.SelfIsNotWindowTop) t.true(results.TopIsNotSame) t.false(results.StackTraces.includes(`at Object.apply`)) }) // NOTE: This test is flaky in CI and temporarily disabled // test('regression: new method will not break recaptcha popup', async t => { // // const browser = await vanillaPuppeteer.launch({ headless: false }) // const browser = await addExtra(vanillaPuppeteer) // .use(Plugin()) // .launch({ headless: true }) // const page = await browser.newPage() // page.waitForTimeout = page.waitForTimeout || page.waitFor // await page.goto('https://www.fbdemo.com/invisible-captcha/index.html') // await page.type('#tswname', 'foo') // await page.type('#tswemail', 'foo@foo.foo') // await page.type( // '#tswcomments', // 'In the depth of winter, I finally learned that within me there lay an invincible summer.' // ) // await page.click('#tswsubmit') // await page.waitForTimeout(1000) // const { hasRecaptchaPopup } = await page.evaluate(() => { // const hasRecaptchaPopup = !!document.querySelectorAll( // `iframe[title="recaptcha challenge"]` // ).length // return { hasRecaptchaPopup } // }) // await browser.close() // t.true(hasRecaptchaPopup) // }) // test('regression: old method indeed did break recaptcha popup', async t => { // const browser = await vanillaPuppeteer.launch({ headless: true }) // const page = await browser.newPage() // page.waitForTimeout = page.waitForTimeout || page.waitFor // // Old method // await page.evaluateOnNewDocument(() => { // // eslint-disable-next-line // Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { // get: function() { // return window // } // }) // }) // await page.goto('https://www.fbdemo.com/invisible-captcha/index.html') // await page.type('#tswname', 'foo') // await page.type('#tswemail', 'foo@foo.foo') // await page.type( // '#tswcomments', // 'In the depth of winter, I finally learned that within me there lay an invincible summer.' // ) // await page.click('#tswsubmit') // await page.waitForTimeout(1000) // const { hasRecaptchaPopup } = await page.evaluate(() => { // const hasRecaptchaPopup = !!document.querySelectorAll( // `iframe[title="recaptcha challenge"]` // ).length // return { hasRecaptchaPopup } // }) // await browser.close() // t.false(hasRecaptchaPopup) // })