EFForg / privacybadger

Privacy Badger is a browser extension that automatically learns to block invisible trackers.
https://privacybadger.org
Other
3.19k stars 386 forks source link

Update conditional content script injection to use faster methods #1865

Open alexristich opened 6 years ago

alexristich commented 6 years ago

Currently we inject content scripts conditionally with message passing, where the content script will query the background page as to whether it should inject itself into the page. This has the potential for the content script to be injected after a page script has already executed. In the case of fingerprinting.js, this means we may not catch an instance of fingerprinting because the fingerprinting occurred before our content script was run.

Mozilla has released a new API (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contentScripts/register) which looks promising. It currently only works for Firefox 59+.

Another alternative is tabs.executeScript() (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/executeScript), which may provide faster injection of content scripts than our current message passing approach.

A final(?) contender is webNavigation.onCommitted (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webNavigation/onCommitted), which, according to Adguard is the fastest means of conditionally injecting a content script: https://github.com/gorhill/uBlock/issues/1930#issuecomment-294486152

ghostwords commented 6 years ago

Related to https://github.com/gbaptista/luminous/issues/55. I'm also reviewing these as part of #1861.

ghostwords commented 6 years ago

I am using this fixture for testing. It comes with an inline script that prints the value of navigator.doNotTrack. Since it's a tiny inline script, anything asynchronous fails to run in time to change its output. If you open the developer console, you'll see the page also continuously prints the value of navigator.doNotTrack every millisecond until it becomes either "1" or "0", which helps you see what your injection delay is.

alexristich commented 6 years ago

I just pushed a work-in-progress using the contentScripts API. A few things to note:

1) This code currently does not work on Chrome. 2) In having conditional injection in Firefox 59+, we by necessity need to have conditional injection in Chrome as well, as we can no longer list the specified content scripts in manifest.json. 3) For some reason, "<all_urls"> is not permitted to be used in the matches key when registering the content scripts. I'll file a Bugzilla issue on this soon. 4) I think passing standardized configuration objects will be challenging for certain scripts. In supercookie.js we have a checkEnabledAndThirdParty query. Checking the enabled state is easy with the contentScripts.register() way of injecting things, given we can check for the absence of a property in the configuration object - this would indicate the script was injected with the contentScripts API. However, determining whether something is third-party wouldn't be possible without receiving some context from the background page. Given this, I believe we would still need to engage message passing in some cases.

I think the next steps are to (a) decide on which approach, tabs.executeScript() or webNavigation.onCommitted(), should be used for conditional injection in browsers that don't yet support the contentScripts API, and (b) figure out a reasonable way to pass configuration to the content scripts. I think the answer for (b) might lie in sending the code as a code string and appending the configuration. Here's some interesting thoughts on this problem in any case: https://stackoverflow.com/a/40815514

alexristich commented 6 years ago

Bug for tracking the second point listed in my comment above: https://bugzilla.mozilla.org/show_bug.cgi?id=1439814

alexristich commented 6 years ago

Following up on this issue, the current issue presented here is the "slow" conditional injection of content scripts. The reason for this "slowness" is that the content scripts need to know whether they should inject themselves into the page. A case where a content script should not inject itself is when the current site is whitelisted by the user.

The way this is currently achieved is using synchronous message passing with chrome.runtime.sendMessage. The proposed solutions above aim to address this, though the optimal solution is to use the new browser.contentScripts API. However, this API is currently only supported in Firefox 59+.

In order to fully leverage this new API, we must either (a) adjust how all current content scripts are declared and injected to avoid duplicate injection, or (b) duplicate all the current scripts, making necessary tweaks to support both methods and avoiding duplicate injection.

Both of these approaches have downsides, though given the limitations presented to content scripts and the requirements of our content scripts (i.e. injected as close to document_start as possible), there doesn't appear to be much room if any for flexibility outside these two options.

fregante commented 5 years ago

I just pushed a work-in-progress using the contentScripts API. A few things to note:

  1. This code currently does not work on Chrome.

You can use the Chrome polyfill for contentScripts.register. If there's interest it can probably be adjusted to work in Firefox 58- (but it might already work there as is)