From 2bdef1be30278e4b4234e0756b575fde1f73e7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:37:01 +0100 Subject: [PATCH 1/9] .gitignore/.dockerignore build artifact `spoof.js` --- .dockerignore | 1 + .gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index c38d3817f..15ead1dc3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,3 +26,4 @@ automation/Extension/firefox/dist automation/Extension/firefox/openwpm.xpi automation/Extension/firefox/src/content.js automation/Extension/firefox/src/feature.js +automation/Extension/firefox/src/spoof.js diff --git a/.gitignore b/.gitignore index 5439718f8..8af07caa9 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ automation/Extension/firefox/dist automation/Extension/firefox/openwpm.xpi automation/Extension/firefox/src/content.js automation/Extension/firefox/src/feature.js +automation/Extension/firefox/src/spoof.js From 80ef59ad6e0731aa5d512c2f950783050d46f6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:43:24 +0100 Subject: [PATCH 2/9] Adjusted Firefox extension structure for a new component The actual executed code is located in "automation/Extension/spoof.js" --- automation/Extension/firefox/feature.js/index.js | 5 +++++ automation/Extension/firefox/spoof.js/index.js | 1 + automation/Extension/firefox/webpack.config.js | 1 + .../src/background/spoof-navigator.ts | 16 ++++++++++++++++ .../webext-instrumentation/src/index.ts | 1 + 5 files changed, 24 insertions(+) create mode 100644 automation/Extension/firefox/spoof.js/index.js create mode 100644 automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts diff --git a/automation/Extension/firefox/feature.js/index.js b/automation/Extension/firefox/feature.js/index.js index 0edfcdddb..789238987 100644 --- a/automation/Extension/firefox/feature.js/index.js +++ b/automation/Extension/firefox/feature.js/index.js @@ -3,6 +3,7 @@ import { JavascriptInstrument, HttpInstrument, NavigationInstrument, + SpoofNavigator, } from "openwpm-webext-instrumentation"; import * as loggingDB from "./loggingdb.js"; @@ -58,6 +59,10 @@ async function main() { httpInstrument.run(config['crawl_id'], config['save_content']); } + + loggingDB.logDebug("Now spoofing `webdriver` attribute to `false`"); // TODO Add config option + let spoofNavigator = new SpoofNavigator(); + await spoofNavigator.registerContentScript(); } main(); diff --git a/automation/Extension/firefox/spoof.js/index.js b/automation/Extension/firefox/spoof.js/index.js new file mode 100644 index 000000000..28aa89304 --- /dev/null +++ b/automation/Extension/firefox/spoof.js/index.js @@ -0,0 +1 @@ +console.log("CODE WOULD BE EXECUTED"); diff --git a/automation/Extension/firefox/webpack.config.js b/automation/Extension/firefox/webpack.config.js index af981bd15..685398bcc 100644 --- a/automation/Extension/firefox/webpack.config.js +++ b/automation/Extension/firefox/webpack.config.js @@ -6,6 +6,7 @@ module.exports = { entry: { feature: "./feature.js/index.js", content: "./content.js/index.js", + spoof: "./spoof.js/index.js", }, output: { path: path.resolve(__dirname, "src"), diff --git a/automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts b/automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts new file mode 100644 index 000000000..2172f9f48 --- /dev/null +++ b/automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts @@ -0,0 +1,16 @@ +export class SpoofNavigator { + /** + * Dynamically register the content script to proxy + * `window.navigator`. The proxy object returns `false` + * for the `window.navigator.webdriver` attribute. + */ + public async registerContentScript() { + return browser.contentScripts.register({ + js: [{ file: "/spoof.js" }], + matches: [""], + allFrames: true, + runAt: "document_start", + matchAboutBlank: true, + }); + } +} diff --git a/automation/Extension/webext-instrumentation/src/index.ts b/automation/Extension/webext-instrumentation/src/index.ts index f0588580b..332ae315f 100644 --- a/automation/Extension/webext-instrumentation/src/index.ts +++ b/automation/Extension/webext-instrumentation/src/index.ts @@ -2,6 +2,7 @@ export * from "./background/cookie-instrument"; export * from "./background/http-instrument"; export * from "./background/javascript-instrument"; export * from "./background/navigation-instrument"; +export * from "./background/spoof-navigator"; export * from "./content/javascript-instrument-content-scope"; export * from "./lib/http-post-parser"; export * from "./lib/string-utils"; From f8c112d00186bba76b2bc185f9961180bcf799ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Mon, 4 Nov 2019 16:14:08 +0100 Subject: [PATCH 3/9] Add browser parameter `spoof_navigator` The parameter equals `false` by default, that is the spoofing is disabled by default. --- automation/Extension/firefox/feature.js/index.js | 9 ++++++--- automation/default_browser_params.json | 1 + crawler.py | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/automation/Extension/firefox/feature.js/index.js b/automation/Extension/firefox/feature.js/index.js index 789238987..e40bc6c93 100644 --- a/automation/Extension/firefox/feature.js/index.js +++ b/automation/Extension/firefox/feature.js/index.js @@ -22,6 +22,7 @@ async function main() { js_instrument:true, js_instrument_modules:"fingerprinting", http_instrument:true, + spoof_navigator:false, save_content:false, testing:true, crawl_id:0 @@ -60,9 +61,11 @@ async function main() { config['save_content']); } - loggingDB.logDebug("Now spoofing `webdriver` attribute to `false`"); // TODO Add config option - let spoofNavigator = new SpoofNavigator(); - await spoofNavigator.registerContentScript(); + if (config['spoof_navigator']) { + loggingDB.logDebug("Now spoofing `webdriver` attribute to `false`"); + let spoofNavigator = new SpoofNavigator(); + await spoofNavigator.registerContentScript(); + } } main(); diff --git a/automation/default_browser_params.json b/automation/default_browser_params.json index 3c26ebd9e..60276dda1 100644 --- a/automation/default_browser_params.json +++ b/automation/default_browser_params.json @@ -5,6 +5,7 @@ "js_instrument_modules": "fingerprinting", "http_instrument": false, "navigation_instrument": false, + "spoof_navigator": false, "save_content": false, "random_attributes": false, diff --git a/crawler.py b/crawler.py index 28a6bb64f..2ba69230e 100644 --- a/crawler.py +++ b/crawler.py @@ -20,6 +20,7 @@ COOKIE_INSTRUMENT = os.getenv('COOKIE_INSTRUMENT', '1') == '1' NAVIGATION_INSTRUMENT = os.getenv('NAVIGATION_INSTRUMENT', '1') == '1' JS_INSTRUMENT = os.getenv('JS_INSTRUMENT', '1') == '1' +SPOOF_NAVIGATOR = os.getenv('SPOOF_NAVIGATOR', '1') == '1' JS_INSTRUMENT_MODULES = os.getenv('JS_INSTRUMENT_MODULES', None) SAVE_CONTENT = os.getenv('SAVE_CONTENT', '') DWELL_TIME = int(os.getenv('DWELL_TIME', '10')) @@ -41,6 +42,7 @@ browser_params[i]['cookie_instrument'] = COOKIE_INSTRUMENT browser_params[i]['navigation_instrument'] = NAVIGATION_INSTRUMENT browser_params[i]['js_instrument'] = JS_INSTRUMENT + browser_params[i]['spoof_navigator'] = SPOOF_NAVIGATOR if JS_INSTRUMENT_MODULES: browser_params[i]['js_instrument_modules'] = JS_INSTRUMENT_MODULES if SAVE_CONTENT == '1': @@ -81,6 +83,7 @@ scope.set_tag('COOKIE_INSTRUMENT', COOKIE_INSTRUMENT) scope.set_tag('NAVIGATION_INSTRUMENT', NAVIGATION_INSTRUMENT) scope.set_tag('JS_INSTRUMENT', JS_INSTRUMENT) + scope.set_tag('SPOOF_NAVIGATOR', SPOOF_NAVIGATOR) scope.set_tag('JS_INSTRUMENT_MODULES', JS_INSTRUMENT) scope.set_tag('SAVE_CONTENT', SAVE_CONTENT) scope.set_tag('DWELL_TIME', DWELL_TIME) From a20449152ec0f308d1466c179b7ba2c29534f2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Tue, 5 Nov 2019 13:52:56 +0100 Subject: [PATCH 4/9] Use JavaScript proxy to spoof `webdriver` attribute --- .../Extension/firefox/spoof.js/index.js | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/automation/Extension/firefox/spoof.js/index.js b/automation/Extension/firefox/spoof.js/index.js index 28aa89304..ebfc8ce51 100644 --- a/automation/Extension/firefox/spoof.js/index.js +++ b/automation/Extension/firefox/spoof.js/index.js @@ -1 +1,74 @@ -console.log("CODE WOULD BE EXECUTED"); +/* +/* This code to spoof values of the `window.navigator` object using a + * JavaScript proxy is based on: + * User Agent Switcher + * Copyright © 2017 – 2019 Alexander Schlarb (https://gitlab.com/ntninja) +/* For the used part see: https://gitlab.com/ntninja/user-agent-switcher/blob/6aacc15ed6651317776f7abb3a85d6f34fc1a254/content/override-navigator-data.js + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Set of all object that we have already proxied to prevent them from being + * proxied twice. + */ +let proxiedObjects = new Set(); + +/** + * Convenience wrapped around `cloneInto` that enables all possible cloning + * options by default. + */ +function cloneIntoFull(value, scope) { + return cloneInto(value, scope, { + cloneFunctions: true, + wrapReflectors: true + }); +} + +/** + * Spoof `navigator` by overwriting the `navigator` of the given + * (content script scope) `window` object with a proxy, if applicable. + */ +function spoofNavigator(window) { + if (!(window instanceof Window)) { // Not actually a window object + return window; + } + + let origNavigator = window.navigator.wrappedJSObject; // `navigator` of the page scope + if (proxiedObjects.has(origNavigator)) { // Window was already shadowed + return window; + } + + let spoofedGet_PageScope = cloneIntoFull({ + get: (target, prop, receiver) => { + if (prop === "webdriver") { + return false; + } else { + return Reflect.get(origNavigator, prop); + } + } + }, window.wrappedJSObject); // The `get` function, defined in privileged code (that is here in the extension / content script), is cloned into the target scope (that is the web page / `window.wrappedJSObject`) and thus accessible there. The return value is the reference to the cloned object in the defined scope. + + let origProxy = window.wrappedJSObject.Proxy; + let navigatorProxy = new origProxy(origNavigator, spoofedGet_PageScope); + + proxiedObjects.add(origNavigator); + + let returnFunc_PageScope = exportFunction(() => { + return navigatorProxy; + }, window.wrappedJSObject); + // Using `__defineGetter__` here our function gets assigned the correct + // name of `get navigator`. Additionally its property descriptor has + // no `set` function and will silently ignore any assigned value. – This + // exact configuration is not achievable using `Object.defineProperty`. + Object.prototype.__defineGetter__.call(window.wrappedJSObject, "navigator", returnFunc_PageScope); + return window; +} + +spoofNavigator(window); From 57a249528078fc504637dde79f2228381a34108f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Tue, 5 Nov 2019 13:54:48 +0100 Subject: [PATCH 5/9] License reference and README adjustment for `navigator` spoofing --- LICENSE | 5 +++++ README.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/LICENSE b/LICENSE index dde2d1a64..855ad5e87 100644 --- a/LICENSE +++ b/LICENSE @@ -41,6 +41,11 @@ https://github.com/redline13/selenium-jmeter By Richard Friedman Licensed GPLv3+ +Incorporating code from User Agent Switcher +https://gitlab.com/ntninja/user-agent-switcher/ +Copyright © 2017 – 2019 Alexander Schlarb +Licensed GPLv3+ + Text of GPLv3 License: ====================== GNU GENERAL PUBLIC LICENSE diff --git a/README.md b/README.md index 6fc55604f..54edef700 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,8 @@ left out of this section. * **NOT SUPPORTED.** See [#101](https://github.com/citp/OpenWPM/issues/101). * Set to `True` to enable Firefox's built-in [Tracking Protection](https://developer.mozilla.org/en-US/Firefox/Privacy/Tracking_Protection). +* `spoof_navigator` + * Set to `True` to spoof the JavaScript DOM attribute `webdriver` to be `false`. Browser Profile Support ----------------------- From f7d35ec408538bbf5d5d19b48f5d1ffe61240f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Thu, 19 Dec 2019 20:31:18 +0100 Subject: [PATCH 6/9] Rename option This renames `spoof_navigator` to `hide_webdriver` to be more descriptive. --- README.md | 4 ++-- automation/Extension/firefox/feature.js/index.js | 12 ++++++------ .../{spoof-navigator.ts => hide-webdriver.ts} | 2 +- .../Extension/webext-instrumentation/src/index.ts | 2 +- automation/default_browser_params.json | 2 +- crawler.py | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) rename automation/Extension/webext-instrumentation/src/background/{spoof-navigator.ts => hide-webdriver.ts} (93%) diff --git a/README.md b/README.md index 54edef700..974a0f053 100644 --- a/README.md +++ b/README.md @@ -296,8 +296,8 @@ left out of this section. * **NOT SUPPORTED.** See [#101](https://github.com/citp/OpenWPM/issues/101). * Set to `True` to enable Firefox's built-in [Tracking Protection](https://developer.mozilla.org/en-US/Firefox/Privacy/Tracking_Protection). -* `spoof_navigator` - * Set to `True` to spoof the JavaScript DOM attribute `webdriver` to be `false`. +* `hide_webdriver` + * Set to `True` to hide that OpenWPM uses webdriver. This option spoofs the JavaScript DOM attribute `navigator.webdriver` to be `false`. Browser Profile Support ----------------------- diff --git a/automation/Extension/firefox/feature.js/index.js b/automation/Extension/firefox/feature.js/index.js index e40bc6c93..7556a3ae6 100644 --- a/automation/Extension/firefox/feature.js/index.js +++ b/automation/Extension/firefox/feature.js/index.js @@ -3,7 +3,7 @@ import { JavascriptInstrument, HttpInstrument, NavigationInstrument, - SpoofNavigator, + HideWebdriver, } from "openwpm-webext-instrumentation"; import * as loggingDB from "./loggingdb.js"; @@ -22,7 +22,7 @@ async function main() { js_instrument:true, js_instrument_modules:"fingerprinting", http_instrument:true, - spoof_navigator:false, + hide_webdriver:false, save_content:false, testing:true, crawl_id:0 @@ -61,10 +61,10 @@ async function main() { config['save_content']); } - if (config['spoof_navigator']) { - loggingDB.logDebug("Now spoofing `webdriver` attribute to `false`"); - let spoofNavigator = new SpoofNavigator(); - await spoofNavigator.registerContentScript(); + if (config['hide_webdriver']) { + loggingDB.logDebug("Hide webdriver enabled"); + let hideWebdriver = new HideWebdriver(); + await hideWebdriver.registerContentScript(); } } diff --git a/automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts b/automation/Extension/webext-instrumentation/src/background/hide-webdriver.ts similarity index 93% rename from automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts rename to automation/Extension/webext-instrumentation/src/background/hide-webdriver.ts index 2172f9f48..338fc8e2b 100644 --- a/automation/Extension/webext-instrumentation/src/background/spoof-navigator.ts +++ b/automation/Extension/webext-instrumentation/src/background/hide-webdriver.ts @@ -1,4 +1,4 @@ -export class SpoofNavigator { +export class HideWebdriver { /** * Dynamically register the content script to proxy * `window.navigator`. The proxy object returns `false` diff --git a/automation/Extension/webext-instrumentation/src/index.ts b/automation/Extension/webext-instrumentation/src/index.ts index 332ae315f..682bed10f 100644 --- a/automation/Extension/webext-instrumentation/src/index.ts +++ b/automation/Extension/webext-instrumentation/src/index.ts @@ -2,7 +2,7 @@ export * from "./background/cookie-instrument"; export * from "./background/http-instrument"; export * from "./background/javascript-instrument"; export * from "./background/navigation-instrument"; -export * from "./background/spoof-navigator"; +export * from "./background/hide-webdriver"; export * from "./content/javascript-instrument-content-scope"; export * from "./lib/http-post-parser"; export * from "./lib/string-utils"; diff --git a/automation/default_browser_params.json b/automation/default_browser_params.json index 60276dda1..38d624943 100644 --- a/automation/default_browser_params.json +++ b/automation/default_browser_params.json @@ -5,7 +5,7 @@ "js_instrument_modules": "fingerprinting", "http_instrument": false, "navigation_instrument": false, - "spoof_navigator": false, + "hide_webdriver": false, "save_content": false, "random_attributes": false, diff --git a/crawler.py b/crawler.py index 2ba69230e..b72dccffd 100644 --- a/crawler.py +++ b/crawler.py @@ -20,7 +20,7 @@ COOKIE_INSTRUMENT = os.getenv('COOKIE_INSTRUMENT', '1') == '1' NAVIGATION_INSTRUMENT = os.getenv('NAVIGATION_INSTRUMENT', '1') == '1' JS_INSTRUMENT = os.getenv('JS_INSTRUMENT', '1') == '1' -SPOOF_NAVIGATOR = os.getenv('SPOOF_NAVIGATOR', '1') == '1' +HIDE_WEBDRIVER = os.getenv('HIDE_WEBDRIVER', '1') == '1' JS_INSTRUMENT_MODULES = os.getenv('JS_INSTRUMENT_MODULES', None) SAVE_CONTENT = os.getenv('SAVE_CONTENT', '') DWELL_TIME = int(os.getenv('DWELL_TIME', '10')) @@ -42,7 +42,7 @@ browser_params[i]['cookie_instrument'] = COOKIE_INSTRUMENT browser_params[i]['navigation_instrument'] = NAVIGATION_INSTRUMENT browser_params[i]['js_instrument'] = JS_INSTRUMENT - browser_params[i]['spoof_navigator'] = SPOOF_NAVIGATOR + browser_params[i]['hide_webdriver'] = HIDE_WEBDRIVER if JS_INSTRUMENT_MODULES: browser_params[i]['js_instrument_modules'] = JS_INSTRUMENT_MODULES if SAVE_CONTENT == '1': @@ -83,7 +83,7 @@ scope.set_tag('COOKIE_INSTRUMENT', COOKIE_INSTRUMENT) scope.set_tag('NAVIGATION_INSTRUMENT', NAVIGATION_INSTRUMENT) scope.set_tag('JS_INSTRUMENT', JS_INSTRUMENT) - scope.set_tag('SPOOF_NAVIGATOR', SPOOF_NAVIGATOR) + scope.set_tag('HIDE_WEBDRIVER', HIDE_WEBDRIVER) scope.set_tag('JS_INSTRUMENT_MODULES', JS_INSTRUMENT) scope.set_tag('SAVE_CONTENT', SAVE_CONTENT) scope.set_tag('DWELL_TIME', DWELL_TIME) From ce75907d98571f4ebfb34239e8ef3a7c8a1e6e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Fri, 20 Dec 2019 17:08:47 +0100 Subject: [PATCH 7/9] Fix problem with broken functions Previously, `navigator` functions such as `javaEnabled` were not proxied correctly and lead to broken pages (for example embedded YouTube videos). --- automation/Extension/firefox/spoof.js/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/automation/Extension/firefox/spoof.js/index.js b/automation/Extension/firefox/spoof.js/index.js index ebfc8ce51..54facb60a 100644 --- a/automation/Extension/firefox/spoof.js/index.js +++ b/automation/Extension/firefox/spoof.js/index.js @@ -50,7 +50,12 @@ function spoofNavigator(window) { if (prop === "webdriver") { return false; } else { - return Reflect.get(origNavigator, prop); + let value = Reflect.get(origNavigator, prop); + if(typeof(value) === "function") { // Bind functions like `navigator.javaEnabled()` to the orginal object in the page scope to allow them to execute + let boundFunc = Function.prototype.bind.call(value, origNavigator); // `value` is used as `this` to call the `bind` function that creates a copy of `Function.prototype` that always runs in the `this` context `origiNavigator` + value = cloneIntoFull(boundFunc, window.wrappedJSObject); + } + return value; } } }, window.wrappedJSObject); // The `get` function, defined in privileged code (that is here in the extension / content script), is cloned into the target scope (that is the web page / `window.wrappedJSObject`) and thus accessible there. The return value is the reference to the cloned object in the defined scope. From 4ca525ca0200765f3d4a70a23d15516c2498344e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Thu, 23 Jan 2020 08:40:37 +0100 Subject: [PATCH 8/9] Enable Firefox preference for pop-up window prevention We suspect this setting is set to `false` by Selenium. It is now manually overwritten such that pop-up windows are blocked by default. See discussion in #526. --- automation/DeployBrowsers/configure_firefox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/automation/DeployBrowsers/configure_firefox.py b/automation/DeployBrowsers/configure_firefox.py index 8e20b7f39..6cef22784 100644 --- a/automation/DeployBrowsers/configure_firefox.py +++ b/automation/DeployBrowsers/configure_firefox.py @@ -211,3 +211,6 @@ def optimize_prefs(fo): # Enable legacy extensions and disable extension signing fo.set_preference("extensions.legacy.enabled", True) fo.set_preference("xpinstall.signatures.required", False) + + # Enable prevention against pop-up windows/tabs (`window.open('')`) + fo.set_preference("dom.disable_open_during_load", True) From 84e4b4811b4bcc8dca4f817e18252d97c850a02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Go=C3=9Fen?= <33035992+Flnch@users.noreply.github.com> Date: Thu, 23 Jan 2020 09:08:30 +0100 Subject: [PATCH 9/9] Integrate best effort workaround for iframes Previously, in iframes the original `navigator` values could be accessed. Now, this is only possible via `window.frames[0]`/`window[0]` (which is the same). Again thanks to Alexander Schlarbs User Agent Switcher extension for the code. --- .../Extension/firefox/spoof.js/index.js | 178 +++++++++++++----- 1 file changed, 136 insertions(+), 42 deletions(-) diff --git a/automation/Extension/firefox/spoof.js/index.js b/automation/Extension/firefox/spoof.js/index.js index 54facb60a..89d658479 100644 --- a/automation/Extension/firefox/spoof.js/index.js +++ b/automation/Extension/firefox/spoof.js/index.js @@ -1,9 +1,9 @@ -/* -/* This code to spoof values of the `window.navigator` object using a +/** + * This code to spoof values of the `window.navigator` object using a * JavaScript proxy is based on: * User Agent Switcher - * Copyright © 2017 – 2019 Alexander Schlarb (https://gitlab.com/ntninja) -/* For the used part see: https://gitlab.com/ntninja/user-agent-switcher/blob/6aacc15ed6651317776f7abb3a85d6f34fc1a254/content/override-navigator-data.js + * Copyright © 2017 – 2019 Alexander Schlarb (https://gitlab.com/ntninja) + * For the used part see: https://gitlab.com/ntninja/user-agent-switcher/blob/6aacc15ed6651317776f7abb3a85d6f34fc1a254/content/override-navigator-data.js * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -11,7 +11,7 @@ * (at your option) any later version. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * along with this program. If not, see . */ /** @@ -25,10 +25,10 @@ let proxiedObjects = new Set(); * options by default. */ function cloneIntoFull(value, scope) { - return cloneInto(value, scope, { - cloneFunctions: true, - wrapReflectors: true - }); + return cloneInto(value, scope, { + cloneFunctions: true, + wrapReflectors: true + }); } /** @@ -36,44 +36,138 @@ function cloneIntoFull(value, scope) { * (content script scope) `window` object with a proxy, if applicable. */ function spoofNavigator(window) { - if (!(window instanceof Window)) { // Not actually a window object - return window; - } + if (!(window instanceof Window)) { // Not actually a window object + return window; + } + + let origNavigator = window.navigator.wrappedJSObject; // `navigator` of the page scope + if (proxiedObjects.has(origNavigator)) { // Window was already shadowed + return window; + } - let origNavigator = window.navigator.wrappedJSObject; // `navigator` of the page scope - if (proxiedObjects.has(origNavigator)) { // Window was already shadowed - return window; - } + let spoofedGet_PageScope = cloneIntoFull({ + get: (target, prop, receiver) => { + if (prop === "webdriver") { + return false; + } else { + let value = Reflect.get(origNavigator, prop); + if(typeof(value) === "function") { // Bind functions like `navigator.javaEnabled()` to the orginal object in the page scope to allow them to execute + let boundFunc = Function.prototype.bind.call(value, origNavigator); // `value` is used as `this` to call the `bind` function that creates a copy of `Function.prototype` that always runs in the `this` context `origiNavigator` + value = cloneIntoFull(boundFunc, window.wrappedJSObject); + } + return value; + } + } + }, window.wrappedJSObject); // The `get` function, defined in privileged code (that is here in the extension / content script), is cloned into the target scope (that is the web page / `window.wrappedJSObject`) and thus accessible there. The return value is the reference to the cloned object in the defined scope. - let spoofedGet_PageScope = cloneIntoFull({ - get: (target, prop, receiver) => { - if (prop === "webdriver") { - return false; - } else { - let value = Reflect.get(origNavigator, prop); - if(typeof(value) === "function") { // Bind functions like `navigator.javaEnabled()` to the orginal object in the page scope to allow them to execute - let boundFunc = Function.prototype.bind.call(value, origNavigator); // `value` is used as `this` to call the `bind` function that creates a copy of `Function.prototype` that always runs in the `this` context `origiNavigator` - value = cloneIntoFull(boundFunc, window.wrappedJSObject); - } - return value; - } - } - }, window.wrappedJSObject); // The `get` function, defined in privileged code (that is here in the extension / content script), is cloned into the target scope (that is the web page / `window.wrappedJSObject`) and thus accessible there. The return value is the reference to the cloned object in the defined scope. + let origProxy = window.wrappedJSObject.Proxy; + let navigatorProxy = new origProxy(origNavigator, spoofedGet_PageScope); - let origProxy = window.wrappedJSObject.Proxy; - let navigatorProxy = new origProxy(origNavigator, spoofedGet_PageScope); + proxiedObjects.add(origNavigator); - proxiedObjects.add(origNavigator); + let returnFunc_PageScope = exportFunction(() => { + return navigatorProxy; + }, window.wrappedJSObject); + // Using `__defineGetter__` here our function gets assigned the correct + // name of `get navigator`. Additionally its property descriptor has + // no `set` function and will silently ignore any assigned value. – This + // exact configuration is not achievable using `Object.defineProperty`. + Object.prototype.__defineGetter__.call(window.wrappedJSObject, "navigator", returnFunc_PageScope); + return window; +} - let returnFunc_PageScope = exportFunction(() => { - return navigatorProxy; - }, window.wrappedJSObject); - // Using `__defineGetter__` here our function gets assigned the correct - // name of `get navigator`. Additionally its property descriptor has - // no `set` function and will silently ignore any assigned value. – This - // exact configuration is not achievable using `Object.defineProperty`. - Object.prototype.__defineGetter__.call(window.wrappedJSObject, "navigator", returnFunc_PageScope); - return window; +/** + * Override `navigator` with the given data on the given page scoped `window` + * object if applicable + * + * This will convert the given `window` object to being content-script scoped + * after checking whether it can be converted at all or is just a restricted + * accessor that does not grant access to anything important. + */ +function spoofNavigatorFromPageScope(unsafeWindow) { + if(!(unsafeWindow instanceof Window)) { + return unsafeWindow; // Not actually a window object + } + + try { + unsafeWindow.navigator; // This will throw if this is a cross-origin frame + + let windowObj = cloneIntoFull(unsafeWindow, window); + return spoofNavigator(windowObj).wrappedJSObject; + } catch(e) { + if(e instanceof DOMException && e.name == "SecurityError") { + // Ignore error created by accessing a cross-origin frame and + // just return the restricted frame (`navigator` is inaccessible + // on these so there is nothing to patch) + return unsafeWindow; + } else { + throw e; + } + } } + spoofNavigator(window); + +// Use some prototype hacking to prevent access to the original `navigator` +// through the IFrame leak +const IFRAME_TYPES = Object.freeze([HTMLFrameElement, HTMLIFrameElement]); +for(let type of IFRAME_TYPES) { + // Get reference to contentWindow & contentDocument accessors into the + // content script scope + let contentWindowGetter = Reflect.getOwnPropertyDescriptor( + type.prototype.wrappedJSObject, "contentWindow" + ).get; + contentWindowGetter = cloneIntoFull(contentWindowGetter, window); + let contentDocumentGetter = Reflect.getOwnPropertyDescriptor( + type.prototype.wrappedJSObject, "contentDocument" + ).get; + contentDocumentGetter = cloneIntoFull(contentDocumentGetter, window); + + // Export compatible accessor on the property that patches the navigator + // element before returning + Object.prototype.__defineGetter__.call(type.prototype.wrappedJSObject, "contentWindow", + exportFunction(function () { + let contentWindow = contentWindowGetter.call(this); + return spoofNavigatorFromPageScope(contentWindow); + }, window.wrappedJSObject) + ); + Object.prototype.__defineGetter__.call(type.prototype.wrappedJSObject, "contentDocument", + exportFunction(function () { + let contentDocument = contentDocumentGetter.call(this); + if(contentDocument !== null) { + spoofNavigatorFromPageScope(contentDocument.defaultView); + } + return contentDocument; + }, window.wrappedJSObject) + ); +} + +// Asynchrously track added IFrame elements and trigger their prototype +// properties defined above to ensure that they are patched +// (This is a best-effort workaround for us being unable to *properly* fix the `window[0]` case.) +let patchNodes = (nodes) => { + for(let node of nodes) { + let isNodeFrameType = false; + for(let type of IFRAME_TYPES) { + if(isNodeFrameType = (node instanceof type)){ break; } + } + if(!isNodeFrameType) { + continue; + } + + node.contentWindow; + node.contentDocument; + } +}; +let observer = new MutationObserver((mutations) => { + for(let mutation of mutations) { + patchNodes(mutation.addedNodes); + } +}); +observer.observe(document.documentElement, { + childList: true, + subtree: true +}); +patchNodes(document.querySelectorAll("frame,iframe")); +