inject-fingerprint
Advanced tools
Comparing version 1.0.7 to 1.1.0
@@ -49,20 +49,20 @@ const { join } = require('path'); | ||
return ` | ||
Object.defineProperty(window.navigator, 'plugins', { | ||
get: function () { | ||
const pluginData = [ | ||
{ name: "Chrome PDF Plugin", filename: "internal-pdf-viewer", description: "Portable Document Format" }, | ||
{ name: "Chrome PDF Viewer", filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", description: "" }, | ||
{ name: "Native Client", filename: "internal-nacl-plugin", description: "" }, | ||
] | ||
const pluginArray = [] | ||
pluginData.forEach(p => { | ||
function FakePlugin() { return p } | ||
const plugin = new FakePlugin() | ||
Object.setPrototypeOf(plugin, Plugin.prototype); | ||
pluginArray.push(plugin) | ||
}) | ||
Object.setPrototypeOf(pluginArray, PluginArray.prototype); | ||
return pluginArray | ||
}, | ||
});`; | ||
Object.defineProperty(window.navigator, 'plugins', { | ||
get: function () { | ||
const pluginData = [ | ||
{ name: "Chrome PDF Plugin", filename: "internal-pdf-viewer", description: "Portable Document Format" }, | ||
{ name: "Chrome PDF Viewer", filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", description: "" }, | ||
{ name: "Native Client", filename: "internal-nacl-plugin", description: "" }, | ||
] | ||
const pluginArray = [] | ||
pluginData.forEach(p => { | ||
function FakePlugin() { return p } | ||
const plugin = new FakePlugin() | ||
Object.setPrototypeOf(plugin, Plugin.prototype); | ||
pluginArray.push(plugin) | ||
}) | ||
Object.setPrototypeOf(pluginArray, PluginArray.prototype); | ||
return pluginArray | ||
}, | ||
});`; | ||
}; | ||
@@ -72,15 +72,15 @@ | ||
return ` | ||
const getParameter = WebGLRenderingContext.getParameter; | ||
WebGLRenderingContext.prototype.getParameter = function (parameter) { | ||
// UNMASKED_VENDOR_WEBGL | ||
if (parameter === 37445) { | ||
return 'Intel Open Source Technology Center'; | ||
} | ||
// UNMASKED_RENDERER_WEBGL | ||
if (parameter === 37446) { | ||
return 'Mesa DRI Intel(R) Ivybridge Mobile '; | ||
} | ||
const getParameter = WebGLRenderingContext.getParameter; | ||
WebGLRenderingContext.prototype.getParameter = function (parameter) { | ||
// UNMASKED_VENDOR_WEBGL | ||
if (parameter === 37445) { | ||
return 'Intel Open Source Technology Center'; | ||
} | ||
// UNMASKED_RENDERER_WEBGL | ||
if (parameter === 37446) { | ||
return 'Mesa DRI Intel(R) Ivybridge Mobile '; | ||
} | ||
return getParameter(parameter); | ||
};`; | ||
return getParameter(parameter); | ||
};`; | ||
}; | ||
@@ -90,6 +90,6 @@ | ||
return ` | ||
const originalQuery = window.navigator.permissions.query; | ||
window.navigator.permissions.query = (parameters) => (parameters.name === 'notifications' | ||
? Promise.resolve({ state: Notification.permission }) | ||
: originalQuery(parameters));`; | ||
const originalQuery = window.navigator.permissions.query; | ||
window.navigator.permissions.query = (parameters) => (parameters.name === 'notifications' | ||
? Promise.resolve({ state: Notification.permission }) | ||
: originalQuery(parameters));`; | ||
}; | ||
@@ -100,31 +100,150 @@ | ||
return ` | ||
function changeProperty(parent, attribute, values) { | ||
Object.defineProperty(window[parent], attribute, { | ||
function changeProperty(parent, attribute, values) { | ||
Object.defineProperty(window[parent], attribute, { | ||
get: function () { | ||
return values | ||
} | ||
}); | ||
} | ||
this.changeProperty('navigator', 'languages', ${JSON.stringify(languages)}) | ||
this.changeProperty('navigator', 'deviceMemory', ${getDeviceMemory()}) | ||
this.changeProperty('navigator', 'hardwareConcurrency', ${getHardwareConcurrency()}) | ||
this.changeProperty('navigator', 'chrome', { runtime: {}, }); | ||
this.changeProperty('navigator', 'appCodeName', 'Mozilla'); | ||
this.changeProperty('navigator', 'platform', 'Linux x86_64'); | ||
this.changeProperty('navigator', 'vendor', 'Google Inc.'); | ||
this.changeProperty('navigator', 'appName', 'Netscape'); | ||
this.changeProperty('window', 'chrome', { runtime: {}, }); | ||
this.changeProperty('screen', 'colorDepth', 24);`; | ||
}; | ||
const bypassWebdriver = () => { | ||
return 'delete navigator.__proto__?.webdriver;'; | ||
}; | ||
const bypassIframe = () => { | ||
return ` | ||
try { | ||
// Adds a contentWindow proxy to the provided iframe element | ||
const addContentWindowProxy = iframe => { | ||
const contentWindowProxy = { | ||
get(target, key) { | ||
// Now to the interesting part: | ||
// We actually make this thing behave like a regular iframe window, | ||
// by intercepting calls to e.g. .self and redirect it to the correct thing. :) | ||
// That makes it possible for these assertions to be correct: | ||
// iframe.contentWindow.self === window.top // must be false | ||
if (key === 'self') { | ||
return this | ||
} | ||
// iframe.contentWindow.frameElement === iframe // must be true | ||
if (key === 'frameElement') { | ||
return iframe | ||
} | ||
return Reflect.get(target, key) | ||
} | ||
} | ||
if (!iframe.contentWindow) { | ||
const proxy = new Proxy(window, contentWindowProxy) | ||
Object.defineProperty(iframe, 'contentWindow', { | ||
get() { | ||
return proxy | ||
}, | ||
set(newValue) { | ||
return newValue // contentWindow is immutable | ||
}, | ||
enumerable: true, | ||
configurable: false | ||
}) | ||
} | ||
} | ||
// Handles iframe element creation, augments srcdoc property so we can intercept further | ||
const handleIframeCreation = (target, thisArg, args) => { | ||
const iframe = target.apply(thisArg, args) | ||
// We need to keep the originals around | ||
const _iframe = iframe | ||
const _srcdoc = _iframe.srcdoc | ||
// Add hook for the srcdoc property | ||
// We need to be very surgical here to not break other iframes by accident | ||
Object.defineProperty(iframe, 'srcdoc', { | ||
configurable: true, // Important, so we can reset this later | ||
get: function () { | ||
return values | ||
return _iframe.srcdoc | ||
}, | ||
set: function (newValue) { | ||
addContentWindowProxy(this) | ||
// Reset property, the hook is only needed once | ||
Object.defineProperty(iframe, 'srcdoc', { | ||
configurable: false, | ||
writable: false, | ||
value: _srcdoc | ||
}) | ||
_iframe.srcdoc = newValue | ||
} | ||
}); | ||
}) | ||
return iframe | ||
} | ||
this.changeProperty('navigator', 'languages', ${JSON.stringify(languages)}) | ||
this.changeProperty('navigator', 'deviceMemory', ${getDeviceMemory()}) | ||
this.changeProperty('navigator', 'hardwareConcurrency', ${getHardwareConcurrency()}) | ||
this.changeProperty('navigator', 'chrome', { runtime: {}, }); | ||
this.changeProperty('navigator', 'appCodeName', 'Mozilla'); | ||
this.changeProperty('navigator', 'platform', 'Linux x86_64'); | ||
this.changeProperty('navigator', 'vendor', 'Google Inc.'); | ||
this.changeProperty('navigator', 'appName', 'Netscape'); | ||
this.changeProperty('window', 'chrome', { runtime: {}, }); | ||
this.changeProperty('screen', 'colorDepth', 24);`; | ||
// Adds a hook to intercept iframe creation events | ||
const addIframeCreationSniffer = () => { | ||
/* global document */ | ||
const createElementHandler = { | ||
// Make toString() native | ||
get(target, key) { | ||
return Reflect.get(target, key) | ||
}, | ||
apply: function (target, thisArg, args) { | ||
const isIframe = | ||
args && args.length && String(args[0]).toLowerCase() === 'iframe' | ||
if (!isIframe) { | ||
// Everything as usual | ||
return target.apply(thisArg, args) | ||
} else { | ||
return handleIframeCreation(target, thisArg, args) | ||
} | ||
} | ||
} | ||
// All this just due to iframes with srcdoc bug | ||
utils.replaceWithProxy( | ||
document, | ||
'createElement', | ||
createElementHandler | ||
) | ||
} | ||
// Let's go | ||
addIframeCreationSniffer() | ||
} catch (err) { | ||
// console.warn(err) | ||
}`; | ||
}; | ||
const bypassWebdriver = () => { | ||
return 'delete navigator.__proto__?.webdriver;'; | ||
const bypassHairlineFeature = () => { | ||
return `// store the existing descriptor | ||
const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight'); | ||
// redefine the property with a patched descriptor | ||
Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', { | ||
...elementDescriptor, | ||
get: function () { | ||
if (this.id === 'modernizr') { | ||
return 1; | ||
} | ||
return elementDescriptor.get.apply(this); | ||
}, | ||
});`; | ||
}; | ||
const createFingerPrintScript = () => { | ||
return `${bypassPlugins()} | ||
return `${readFileSync('./utils-finger-printer.js')} | ||
${bypassPlugins()} | ||
${bypassChangeProperties()} | ||
${bypassPermissions()} | ||
${bypassWebGL()} | ||
${bypassWebdriver()}`; | ||
${bypassIframe()} | ||
${bypassWebdriver()} | ||
${bypassHairlineFeature()}`; | ||
}; | ||
@@ -131,0 +250,0 @@ |
{ | ||
"name": "inject-fingerprint", | ||
"version": "1.0.7", | ||
"version": "1.1.0", | ||
"description": "Create Internal proxy to inject fingerprint", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -1,14 +0,17 @@ | ||
const { writeFileSync, existsSync } = require('fs'); | ||
const { join } = require('path'); | ||
const { existsSync } = require('fs'); | ||
const { expect } = require('chai'); | ||
const { By, until } = require('selenium-webdriver'); | ||
const ProxyServer = new (require('../index'))({ logLevel: 'silly' }); | ||
let driver; | ||
let driver, tables; | ||
describe('Page fingerprint application validation test', async () => { | ||
before(() => { | ||
before(async () => { | ||
ProxyServer.start(); | ||
driver = ProxyServer.DriverBuilder(ProxyServer.options).build(); | ||
await driver.get('https://bot.sannysoft.com/'); | ||
await driver.wait(until.elementLocated(By.xpath('//*[@id="fp2"]')), 10000); | ||
tables = await driver.findElements(By.css('table')); | ||
}); | ||
after(() => { | ||
@@ -19,8 +22,7 @@ ProxyServer.close(); | ||
it('Should run fingerprint tests on test page and validate results', async () => { | ||
await driver.get('https://bot.sannysoft.com/'); | ||
await driver.wait(until.elementLocated(By.xpath('//*[@id="fp2"]')), 10000); | ||
await new Screenshot(driver).take('test'); | ||
const tables = await driver.findElements(By.css('table')); | ||
it('Should run old fingerprint tests on test page and validate results', async () => { | ||
await oldFingerPrintValidate(tables); | ||
}); | ||
it('Should run new fingerprint tests on test page and validate results', async () => { | ||
await newFingerPrintValidate(tables); | ||
@@ -51,3 +53,3 @@ }); | ||
const failedEls = await line.findElements(By.className('failed')); | ||
if (failedEls.length != 0 && !name.includes('Hairline Feature'))//Bypass desnessário | ||
if (failedEls.length != 0) | ||
failedTests.push({ name, result }); | ||
@@ -57,3 +59,3 @@ } | ||
expect(failedTests, `[FAIL] Old FingerPrint: ${JSON.stringify(failedTests)}`).to.be.empty; | ||
expect(passedTests.length, '[SUCCESS] Old FingerPrint').to.be.equal(11); | ||
expect(passedTests.length, '[SUCCESS] Old FingerPrint').to.be.equal(12); | ||
} | ||
@@ -63,7 +65,6 @@ | ||
const newFingerprintLines = await tables[1].findElements(By.css('tr')); | ||
const passedTests = [], failedTests = []; | ||
const passedTests = [], warnTests = [], failedTests = []; | ||
for (const line of newFingerprintLines) { | ||
const tds = await line.findElements(By.css('td')); | ||
const key = await tds[0].getText(); | ||
if (key === 'HEADCHR_IFRAME') continue;//Sem bypass por enquanto | ||
const status = await tds[1].getText(); | ||
@@ -75,3 +76,4 @@ const value = await tds[2].getText(); | ||
passedTests.push(fingerResult); | ||
else if (status == 'WARN') | ||
warnTests.push(fingerResult); | ||
else | ||
@@ -81,39 +83,29 @@ failedTests.push(fingerResult); | ||
expect(failedTests, `[FAIL] New FingerPrint: ${JSON.stringify(failedTests)}`).to.be.empty; | ||
expect(passedTests.length, '[SUCCESS] New FingerPrint').to.be.equal(20); | ||
} | ||
const explanation = ` | ||
PHANTOM_UA: Detect PhantomJS user agent | ||
PHANTOM_PROPERTIES: Test the presence of properties introduced by PhantomJS | ||
PHANTOM_ETSL: Runtime verification for PhantomJS | ||
PHANTOM_LANGUAGE: Use navigator.languages to detect PhantomJS | ||
PHANTOM_WEBSOCKET: Analyze the error thrown when creating a websocket | ||
MQ_SCREEN: Use media query related to the screen | ||
PHANTOM_OVERFLOW: Analyze error thrown when a stack overflow occurs | ||
PHANTOM_WINDOW_HEIGHT: Analyze window screen dimension | ||
HEADCHR_UA: Detect Chrome Headless user agent | ||
WEBDRIVER: Test the presence of webriver attributes | ||
HEADCHR_CHROME_OBJ: Test the presence of the window.chrome object | ||
HEADCHR_PERMISSIONS: Test permissions management | ||
HEADCHR_PLUGINS: Verify the number of plugins | ||
HEADCHR_IFRAME: Test presence of Chrome Headless using an iframe | ||
CHR_DEBUG_TOOLS: Test if debug tools are opened | ||
SELENIUM_DRIVER: Test the presence of Selenium drivers | ||
CHR_BATTERY: Test the presence of battery | ||
CHR_MEMORY: Verify if navigator.deviceMemory is consistent | ||
TRANSPARENT_PIXEL: Verify if a canvas pixel is transparent`; | ||
class Screenshot { | ||
__LOGGER_FINGERPRINT && __LOGGER_FINGERPRINT.info(`[proxy-server] Explanation test new fingerprint: '${explanation}'`); | ||
constructor(driver) { | ||
this.driver = driver; | ||
} | ||
if (warnTests) __LOGGER_FINGERPRINT.warn(`[WARN] New FingerPrint: ${JSON.stringify(warnTests)}`); | ||
/** | ||
* @param {String} path Path to save photo | ||
* @returns {Promise} | ||
*/ | ||
async take(path = './') { | ||
try { | ||
await this._setBackground(); | ||
const image = await this.driver.takeScreenshot(); | ||
const fileName = Date.now() + '.png'; | ||
writeFileSync(join(path, fileName), image.replace(/^data:image\/png;base64,/, ''), 'base64'); | ||
__LOGGER_FINGERPRINT.info(`[screenshot] Screenshot => '${fileName}'`); | ||
return fileName; | ||
} catch (err) { | ||
__LOGGER_FINGERPRINT.warn('[screenshot] Não foi possível salvar screenshot: ' + err.message); | ||
} | ||
} | ||
async _setBackground() { | ||
/* istanbul ignore next */ | ||
await this.driver.executeScript(function () { | ||
const style = document.createElement('style'), | ||
text = document.createTextNode('body { background: #fff }'); | ||
style.setAttribute('type', 'text/css'); | ||
style.appendChild(text); | ||
document.head.insertBefore(style, document.head.firstChild); | ||
}); | ||
} | ||
expect(failedTests, `[FAIL] New FingerPrint: ${JSON.stringify(failedTests)}`).to.be.empty; | ||
expect(passedTests.length, '[SUCCESS] New FingerPrint').to.be.equal(20); | ||
} |
45045
15
875