转载 地址https://intoli-dot-com.ext.jsproxy.tk/blog/not-possible-to-block-chrome-headless/
(直接使用浏览器只带的翻译了)
几个月前,我写了一篇名为Making Chrome Headless Untetectable的热门文章回应了一个名为Detecting Chrome Headless的文章作者:Antione Vastel。我真正想要写的一件事是,基于浏览器指纹识别来阻止网站访问者是一种非常用户敌对的做法。浏览器配置中存在如此多的变化,您不可避免地会阻止对您网站的非自动访问,并且最重要的是 - 您实际上没有完成阻止复杂的Web抓取工具的任何事情。为了说明这一点,我展示了如何在Antione的第一篇文章中绕过所有建议的“测试”,并指出它们尚未在多个浏览器版本中进行测试,并且对于任何具有beta或不稳定Chrome版本的用户都会失败。
那些beta和不稳定版本已经成为稳定版本,Antione发布了他的博客文章的更新版本。有可能检测并阻止Chrome无头,他删除了我指出的依赖于版本的测试,并添加了一些新版本。正如你可能从这篇文章的标题中猜到的那样,我倾向于不同意他的标题。澄清一下:我认为不可能为试图隐藏它的用户检测浏览器自动化,如果他们不想隐藏它,那么它就是微不足道的。
有一个window.navigator.webdriver
属性被定义为WebDriver规范的一部分,用于指示何时使用WebDriver,但它在Chrome中实现,作为任何浏览器自动化的指示。Chrome Intent to Ship的财产摘要如下。
将可枚举,不可配置的readonly属性添加
webdriver
到窗口全局对象的导航器对象。true
如果CommandLine具有“启用自动化”或“无头”切换,则属性。否则就是false
。
所以基本上,const isAutomated = navigator.webdriver;
是你的无头测试(好吧,这真的是测试自动化而不是无头浏览器,但这通常是人们实际上试图做的事情)。一旦超出这个范围,您基本上就会表明您正试图检测那些主动隐藏其浏览器自动化事实的用户。那是不可能的。你可以提出你想要的任何测试,但任何专用的网络刮板都可以轻松绕过它们。
如果您已经阅读过使用Chrome Head Headless Untectable,那么本文应该非常熟悉。我将建立一个测试页面来实现每个Antione的测试,然后展示如何绕过它们是相当微不足道的。我上次使用过Selenium,所以这次我会用Puppeteer来调味它。大多数测试绕过都是作为注入的JavaScript实现的,但是,大部分代码应该适用于支持JavaScript注入的任何浏览器自动化框架(请参阅:使用Selenium,Puppeteer和Marionette的JavaScript注入)。这里开发的所有代码 - 以及运行它的说明 - 也可以在GitHub上找到。如果您想尝试代码并在此处阅读时运行,请首先抓住第一个。
进行测试
我在一个名为chrome-headless-test.html的简单测试页面中实现了Antione提出的每个测试。该页面呈现一个这样的简单表格
每个测试的结果。当页面加载时用于填充表格的实际测试是在chrome-headless-test.js中,但是当我绕过它们时,我将详细介绍每个测试的细节。这里显示的结果是无头浏览器生成的,没有任何隐藏自身的尝试。
正如我之前提到的,我将在整篇文章中使用Puppeteer作为我的自动化框架。Puppeteer提供了一个基于Chrome DevTools协议构建的友好的高级JavaScript API ,它是我最喜欢的自动化框架之一。它可以通过运行安装,yarn install puppeteer
它将自己下载它自己的Chromium构建,node_modules
以便在自动化过程中使用。
访问测试页面并使用Puppeteer记录测试结果的代码非常简单。我们基本上只是以无头模式启动浏览器,访问测试页面,获取结果表的屏幕截图,然后退出。执行此操作的代码在test-headless-initial.js中可用,其内容如下所示。
// We'll use Puppeteer is our browser automation framework.
const puppeteer = require('puppeteer');
// This is where we'll put the code to get around the tests.
const preparePageForTests = async (page) => {
// TODO: Not implemented yet.
}
(async () => {
// Launch the browser in headless mode and set up a page.
const browser = await puppeteer.launch({
args: ['--no-sandbox'],
headless: true,
});
const page = await browser.newPage();
// Prepare for the tests (not yet implemented).
await preparePageForTests(page);
// Navigate to the page that will perform the tests.
const testUrl = 'https://intoli.com/blog/' +
'not-possible-to-block-chrome-headless/chrome-headless-test.html';
await page.goto(testUrl);
// Save a screenshot of the results.
await page.screenshot({path: 'headless-test-result.png'});
// Clean up.
await browser.close()
})();
这可以运行,node test-headless-initial.js
它将生成一个headless-test-results.png
文件,其中包含我们之前看到的结果表。
您会注意到preparePageForTests
文件顶部定义的方法当前无效。在本文的其余部分中,我们将逐一完成测试并展示如何绕过它们。如果您想一次查看所有代码,最终结果可以在test-headless-final.js中找到。
Intoli Smart Proxies
如果您正在进行严肃的网页抓取,那么使用代理是必须的。我们的Intoli Smart Proxies服务可让您轻松停止被机器人缓解服务阻止。您的请求可以通过干净的住宅IP智能路由,它们可能会成功,失败的请求会自动重试,您甚至可以通过预先加载自定义功能的远程浏览器发出请求,这样很难检测到您的刮刀。在下面输入您的电子邮件地址,以便访问我们用于所有网络抓取的相同工具!
入门
绕过测试
让我们现在逐个完成并绕过每个测试。请记住,在每个部分中开发的所有代码都旨在进入preparePageForTests
访问测试页面之前调用的方法。这意味着该page
对象可用,我们在一个async
函数内(所以我们可以使用await
)。如果你对任何部件如何组合在一起感到困惑,那么只需看看test-headless-final.js。
用户代理测试
// User-Agent Test
if (/HeadlessChrome/.test(navigator.userAgent)) {
// Test Failed...
}
这是自动访问的经典测试,它早在无头浏览器甚至浏览器自动化框架之前就已存在(至少用于服务器端检查)。它具有相对较低的假阳性率,但它也是欺骗或绕过最琐碎的事情。命令行工具喜欢curl
并wget
提供标志来更改User-Agent
标题,Chrome也不例外。你可以--user-agent
在启动Chrome-headless或其他方式时指定标志,它将修改User-Agent
标题和navigator.userAgent
对象。
Puppeteer还提供了一个setUserAgent()方法,可用于完成同样的事情。只需添加即可
// Pass the User-Agent Test.
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64)' +
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36';
await page.setUserAgent(userAgent);
我们的preparePageForTests()
方法,这个测试通过。
Webdriver测试
// Webdriver Test
if (navigator.webdriver) {
// Test Failed...
}
由于几个原因,这是一个有趣的测试。这是唯一一个具有相对较低的误报率的测试(尽管存在依赖于浏览器自动化的可访问性工具)。此外,如果您尝试设置navigator.webdriver = false
为绕过测试的方法,您会看到一些奇怪的行为。您在分配期间没有收到错误,但navigator.webdriver
仍然会在true
之后。
为了解决这个问题,我们需要使用新的getter函数Object.defineProperty()
来重新定义webdriver
属性navigator
。以下是在Chrome DevTools中以交互方式执行此操作的示例。
要添加到我们的preparePageForTests()
方法的相关代码位是
// Pass the Webdriver Test.
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
});
它让我们向前迈进了一步就像我们完全没有头部的考试一样。请注意,我们正在使用Puppeteer的evaluateOnNewDocument()方法来确保在任何页面的JavaScript可以运行之前在页面上下文中重新定义我们的属性。我们将在所有其他测试中使用相同的方法。您可以在我们的JavaScript注入Selenium,Puppeteer和Marionette文章中找到有关它的更多详细信息以及其他浏览器自动化框架的类似方法。
Chrome测试
// Chrome Test
if (!window.chrome) {
// Test Failed...
}
谷歌浏览器目前不支持扩展,并且window.chrome
在无头模式下对象未定义(注意:我在本文的早期版本中错误地认为该属性未被定义)。在非无头浏览器中,window.chrome
对象看起来像这样(请记住Web Extensions API在页面上下文中不可用)。
{
app: {
isInstalled: false,
},
webstore: {
onInstallStageChanged: {},
onDownloadProgress: {},
},
runtime: {
PlatformOs: {
MAC: 'mac',
WIN: 'win',
ANDROID: 'android',
CROS: 'cros',
LINUX: 'linux',
OPENBSD: 'openbsd',
},
PlatformArch: {
ARM: 'arm',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
PlatformNaclArch: {
ARM: 'arm',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
RequestUpdateCheckStatus: {
THROTTLED: 'throttled',
NO_UPDATE: 'no_update',
UPDATE_AVAILABLE: 'update_available',
},
OnInstalledReason: {
INSTALL: 'install',
UPDATE: 'update',
CHROME_UPDATE: 'chrome_update',
SHARED_MODULE_UPDATE: 'shared_module_update',
},
OnRestartRequiredReason: {
APP_UPDATE: 'app_update',
OS_UPDATE: 'os_update',
PERIODIC: 'periodic',
},
},
}
基于此,更复杂的测试可以挖掘chrome
对象的结构。
// Chrome Test
if (!window.chrome || !window.chrome.runtime) {
// Test Failed...
}
当然,这仍然是微不足道的绕过。我们只需要模拟测试代码检查的内容。
// Pass the Chrome Test.
await page.evaluateOnNewDocument(() => {
// We can mock this in as much depth as we need for the test.
window.navigator.chrome = {
runtime: {},
// etc.
};
});
流行,preparePageForTests()
你很高兴。
权限测试
// Permissions Test
(async () => {
const permissionStatus = await navigator.permissions.query({ name: 'notifications' });
if(Notification.permission === 'denied' && permissionStatus.state === 'prompt') {
// Test Failed...
}
})();
这个测试背后的想法是Notification.permission
和navigator.permissions.query
报告矛盾价值的结果。我们需要做的就是确保它们是一致的。我们可以通过navigator.permissions.query
用一个行为方式覆盖方法来做到这一点。
// Pass the Permissions Test.
await page.evaluateOnNewDocument(() => {
const originalQuery = window.navigator.permissions.query;
return window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
});
请注意,navigator.permissions.query
如果查询名称不是notifications
,则会传递给原始方法,但如果测试更复杂,则可以类似地处理其他查询。
插件长度测试
// Plugins Length Test
if (navigator.plugins.length === 0) {
pluginsLengthElement.classList.add('failed');
// Test Failed...
}
这是一个特别用户恶意的测试,因为浏览器插件通常被隐藏为隐私措施。Firefox默认执行此操作,并且有一些扩展在Chrome中提供相同的行为。为了欺骗插件,我们可以navigator
像我们一样简单地定义另一个属性getter webdriver
。
// Pass the Plugins Length Test.
await page.evaluateOnNewDocument(() => {
// Overwrite the `plugins` property to use a custom getter.
Object.defineProperty(navigator, 'plugins', {
// This just needs to have `length > 0` for the current test,
// but we could mock the plugins too if necessary.
get: () => [1, 2, 3, 4, 5],
});
});
语言测试
// Languages Test
if (!navigator.languages || navigator.languages.length === 0) {
// Test Failed...
}
最后,我们进入语言测试。这是另一个测试,香草无头Chrome通过我没有问题。如果不深入研究,可能取决于操作系统或其配置。
如果您的配置不通过提供任何语言navigator.languages
,你可以重新定义的属性getter像我们既做plugins
和webdriver
。
// Pass the Languages Test.
await page.evaluateOnNewDocument(() => {
// Overwrite the `plugins` property to use a custom getter.
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
});
});
这是最后的测试,一切都应该在这一点上通过。请务必查看test-headless-final.js以了解所有内容是如何组合在一起的,或者尝试自己运行它。
把它放在一起
<img src="https://intoli-dot-com.ext.jsproxy.tk/blog/not-possible-to-block-chrome-headless/img/headless-final-results.png" alt="绕过测试后的最终结果">
如您所见,我们能够毫无困难地绕过所有这些测试。我们用来绕过这些的脚本显然不会在所有情况下都有效,但这些技术通常是适用的,可以针对站点可能使用的任何特定测试集进行自定义。如果有人想隐瞒他们在您的网站上使用浏览器自动化框架的事实,那么他们将能够。基于浏览器指纹识别阻止用户通常是一个坏主意,并且最终会伤害用户。
如果您发现这篇文章是因为您正在处理自己的一些浏览器自动化问题,请与我们取得联系。我们在这方面有很多经验,我们喜欢在新项目上工作。