[Bug] I maybe found some ways to detect stealth plugin
See original GitHub issueI conducted the tests with the following chrome/chromium browsers on Linux Ubuntu 18.04:
Puppeteer Vanilla Chrome/88.0.4298.0
/ 20.10.2020
npm version: puppeteer-5.5.0
Puppeteer Stealth Chromium version: Chrome/88.0.4298.0
/ 20.10.2020
npm versions: puppeteer-5.5.0
puppeteer-extra-3.1.16
puppeteer-extra-plugin-stealth-2.6.6
Google Chrome version: Chrome/87.0.4280.141
/ 07.01.2021
Chromium version: Chrome/87.0.4280.66
/ 17.11.2020
navigatorPrototype
Test
['hardwareConcurrency', 'languages'].forEach((prop) => {
let objDesc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), prop);
if (objDesc !== undefined) {
if (objDesc.value !== undefined) {
res = objDesc.value.toString();
} else if (objDesc.get !== undefined) {
res = objDesc.get.toString();
}
} else {
res = "";
}
console.log(prop + "~~~" + res)
})
Results
Puppeteer Vanilla hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }
Puppeteer Stealth hardwareConcurrency~~~4
Google Chrome hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }
Chromium "hardwareConcurrency~~~function get hardwareConcurrency() { [native code] }"
Puppeteer Vanilla languages~~~function get languages() { [native code] }
Puppeteer Stealth languages~~~() => opts.languages || ['en-US', 'en']
Google Chrome languages~~~function get languages() { [native code] }
Chromium languages~~~function get languages() { [native code] }
Conclusion
Plugin Stealth seems to change the protoype of navigator.plugins
and navigator.hardwareConcurrency
in way such that it is unique to plugin stealth!
permissions
Test
var permissions = () => {
return new Promise((resolve) => {
navigator.permissions.query({name: 'notifications'}).then((val) => {
resolve({
state: val.state,
permission: Notification.permission
})
});
})
}
permissions().then((res) => console.log(res))
Results
Puppeteer Vanilla { "state": "prompt", "permission": "default" }
Puppeteer Stealth { "state": "default", "permission": "default" }
Google Chrome { "state": "prompt", "permission": "default" }
Chromium { "state": "prompt", "permission": "default" }
Conclusion
Here is the evasion: https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/index.js
state
should be prompt
and not default
. plugin stealth spoofs wrong value and
creates behavior that seems as far as I know unique to plugin stealth.
platform
Test
navigator.platform
Results
Puppeteer Vanilla "Linux x86_64"
Puppeteer Stealth "Win32"
Google Chrome "Linux x86_64"
Chromium "Linux x86_64"
Conclusion
I get why you set Win32
as default. But it is very easy to detect that navigator.platform
is lying,
when the other properties in navigator
are not compatible. Why is it not possible to detect the correct
platform on startup of puppeteer stealth?
plugins
Test
Iterate over navigator.plugins
and build str repr.
Results
Puppeteer Vanilla "Chromium PDF Plugin"
and "Chromium PDF Viewer"
Puppeteer Stealth "Chromium PDF Plugin"
and "Chromium PDF Viewer"
Google Chrome "Chrome PDF Plugin"
and "Chrome PDF Viewer"
Chromium "Chromium PDF Plugin"
and "Chromium PDF Viewer"
Conclusion
If I am not mistaken, at some other evasion you try to hide the fact that plugin stealth is in fact chromium (instead of Google Chrome). You do not do it here. Could be inconsistent.
resOverflow
Test
let depth = 0;
let errorMessage = '';
let errorName = '';
let errorStacklength = 0;
function iWillBetrayYouWithMyLongName() {
try {
depth++;
iWillBetrayYouWithMyLongName();
} catch (e) {
errorMessage = e.message;
errorName = e.name;
errorStacklength = e.stack.toString().length;
}
}
iWillBetrayYouWithMyLongName();
console.log({
depth: depth,
errorMessage: errorMessage,
errorName: errorName,
errorStacklength: errorStacklength
})
Results
Puppeteer Vanilla { "depth": 10465, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Puppeteer Stealth { "depth": 10465, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Google Chrome { "depth": 10476, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 864 }
Chromium { "depth": 10474, "errorMessage": "Maximum call stack size exceeded", "errorName": "RangeError", "errorStacklength": 914 }
Conclusion
Values seem to be stable and equivalent for Puppeteer Vanilla and Puppeteer Stealth, but different when puppeteer is not used…Could this be a way to detect pptr usage regardless whether you use stealth or not?
Edit: This particular deviation in call stack size might be due to different browser versions used.
videoCard
Test
Get video card name code.
Results
Puppeteer Vanilla [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]
Puppeteer Stealth [ "Intel Inc.", "Intel Iris OpenGL Engine" ]
Google Chrome [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]
Chromium [ "Google Inc.", "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) Ivybridge Mobile , OpenGL 4.2 core)" ]
Conclusion
Stealth plugin sets static values that never change. This could be an indicator that puppeteer stealth plugin might be used… Why is this being overwritten? I think vanilla puppeteer is using the correct values?
Issue Analytics
- State:
- Created 3 years ago
- Comments:10 (6 by maintainers)
Top GitHub Comments
I have nothing to add, just want to congratulate @NikolaiT for all his findings. Nice job, dude !
A few thoughts:
navigator
: @berstend is aware of thenavigator
issues, it’s a work in progress.permissions
: Good catch on that one.resOverflow
: This one is interesting, I’m going to look into that - let me know if you test same browser versions as per your edit though.webgl
: You are supposed to set your own string here, the default one is just a default.