Closed Bug 1636629 (CVE-2021-43540) Opened 2 years ago Closed 1 year ago

Extensions can Inject ServiceWorkers which persist after uninstall

Categories

(WebExtensions :: Request Handling, defect, P1)

76 Branch
defect

Tracking

(firefox-esr68 wontfix, firefox-esr78 wontfix, firefox-esr91 wontfix, firefox76 wontfix, firefox77 wontfix, firefox78 wontfix, firefox79 wontfix, firefox93 wontfix, firefox94 wontfix, firefox95 verified)

VERIFIED FIXED
95 Branch
Tracking Status
firefox-esr68 --- wontfix
firefox-esr78 --- wontfix
firefox-esr91 --- wontfix
firefox76 --- wontfix
firefox77 --- wontfix
firefox78 --- wontfix
firefox79 --- wontfix
firefox93 --- wontfix
firefox94 --- wontfix
firefox95 --- verified

People

(Reporter: jake.heath91, Assigned: rpl)

References

Details

(Keywords: sec-moderate, Whiteboard: [post-critsmash-triage][adv-main95+])

Attachments

(7 files)

Attached file background.zip

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0

Steps to reproduce:

Side-load the attached extension and observe that a tab appears loading the URL "https://example.com". Then, navigate to "about:debugging" and notice a ServiceWorker has been installed on the example.com domain with the URL "https://example.com/?injected-sw=1". Remove the side-loaded extension and notice the ServiceWorker is not unloaded.

Actual results:

An extension installed a ServiceWorker into the domain of a page.

Expected results:

This may be by design, but I don't think extensions should be able to inject ServiceWorkers into pages. This would allow extension developers to monitor all HTTP traffic from that domain and would give them persistent access to this monitoring capability even if the extension is uninstalled. Since the ServiceWorker can be installed with any random query parameter, as demonstrated in the code, this is also extremely difficult for the true owners of the domain to remove the ServiceWorker from the user's browser. User's will likely need to remove the ServiceWorker manually.

It should also be noted that these ServiceWorkers will always be around. Due to how they are installed through header and body manipulations, when the ServiceWorker attempts to update, the content-type will not match the "application/javascript" that is expected when the extension is no longer installed. This will cause the browser to fall back to the original ServiceWorker code.

Product: Firefox → WebExtensions

Thank you for the detailed explanation and PoC.

(In reply to Jake Heath from comment #0)

This would allow extension developers to monitor all HTTP traffic from that domain

In order to install a ServiceWorker for the origin, the extension already needs all permissions which would allow it to "monitor all traffic".

Since the ServiceWorker can be installed with any random query parameter, as demonstrated in the code, this is also extremely difficult for the true owners of the domain to remove the ServiceWorker from the user's browser.

I managed to unregister it via the console by reading navigator.serviceWorker.controller and creating a new ServiceWorkerRegistration with the same scriptURL.

It should also be noted that these ServiceWorkers will always be around. Due to how they are installed through header and body manipulations, when the ServiceWorker attempts to update, the content-type will not match the "application/javascript" that is expected when the extension is no longer installed. This will cause the browser to fall back to the original ServiceWorker code.

huh, I guess this is step 7 from the update fetch algorithm:
https://w3c.github.io/ServiceWorker/#ref-for-fetching-scripts-perform-fetch%E2%91%A0

I'm not that familiar with the ServiceWorkers design, but this seems like a foot gun for websites using them even without any malicious extensions. It looks like deleting the whole website and turning it into static content would keep the service worker alive forever, since those request would likely return a text/html 404 error page.

Service Workers are supposed to be revalidated within 24 hours. If it's possible to force a bad cached version to be around indefinitely, then there's an issue with the spec/implementation. One of the reasons for the removal of HPKP is the undesired side effect of breaking websites for too long by an accidental or intentionally malicious action. I would expect/hope that Service workers are immune to this, but I didn't check recently, and didn't get a clear expectation from the spec discussion at https://github.com/w3c/ServiceWorker/issues/514

On the extension-specific part: after uninstalling an extension, it can partially stay active if it managed to inject a script in an existing web page. Short of reloading/discarding all existing tabs, this cannot be resolved. But restarting the browser would generally result in a clean state again.
This report shows that Service workers could be abused to extend the scope of maliciousness.

If we want to mitigate that without changing the API, then we could consider adding a flag to service workers whose response have been tainted by the webRequest.filterResponseData / StreamFilter API, and forcibly unregister such service workers upon extension uninstall.

(In reply to Tomislav Jovanovic :zombie from comment #1)

Thank you for the detailed explanation and PoC.

(In reply to Jake Heath from comment #0)

This would allow extension developers to monitor all HTTP traffic from that domain

In order to install a ServiceWorker for the origin, the extension already needs all permissions which would allow it to "monitor all traffic".

You're correct here. The main point of the issue though is the persistence, maybe a title change is needed. After the extension is uninstalled, the ServiceWorker remains.

Since the ServiceWorker can be installed with any random query parameter, as demonstrated in the code, this is also extremely difficult for the true owners of the domain to remove the ServiceWorker from the user's browser.

I managed to unregister it via the console by reading navigator.serviceWorker.controller and creating a new ServiceWorkerRegistration with the same scriptURL.

O neat, I didn't know of this API. That would help domain owners remove these extensions programmatically.

It should also be noted that these ServiceWorkers will always be around. Due to how they are installed through header and body manipulations, when the ServiceWorker attempts to update, the content-type will not match the "application/javascript" that is expected when the extension is no longer installed. This will cause the browser to fall back to the original ServiceWorker code.

huh, I guess this is step 7 from the update fetch algorithm:
https://w3c.github.io/ServiceWorker/#ref-for-fetching-scripts-perform-fetch%E2%91%A0

I'm not that familiar with the ServiceWorkers design, but this seems like a foot gun for websites using them even without any malicious extensions. It looks like deleting the whole website and turning it into static content would keep the service worker alive forever, since those request would likely return a text/html 404 error page.

This response also speaks to Rob's comment, but this was my thought as well. The ServiceWorker is supposed to update within 24 hours. However, due to step 7 of the update fetch algorithm, the ServiceWorker will never get the correct response "Content-Type" after the extension has been unloaded because the extension purposefully modifies the "Content-Type" header of a known 200 request to be "application/javascript". When it sees "text/html" instead of "application/javascript", for instance, the ServiceWorker update will fail and it will fall back to the existing extension code. When I originally discovered this issue, I decided against writing it up because of this limitation, but it was only recently (several months later), that I noticed my injected service worker was still running in the background. It had never removed itself.

If we want to mitigate that without changing the API, then we could consider adding a flag to service workers whose response have been tainted by the webRequest.filterResponseData / StreamFilter API, and forcibly unregister such service workers upon extension uninstall.

Agreed, I think the fix should be that ServiceWorkers injected via extensions should be unloaded when the extension is unloaded. This sounds like a difficult engineering effort, but one that would be worth it. I can think of so many avenues for extensions to abuse this functionality to leak information about user's who install their extension and I think it would be incredibly difficult to put this burden on domain owners to regularly check for malicious ServiceWorkers that have been installed on the domain.

This bug is part of a group of bugs in a security or private group which have the old default Severity of normal which has not been changed, and the default priority of --. This indicates that this bugs Severity should be set to -- so it will show up in triage lists.

Trying to set that severity again.

Severity: normal → --

What would be the effort in reliably detecting that a service worker was injected via an extension (assuming that's feasible)? Having SWs that were installed via an extension live and die with the extension sounds attractive.

Flags: needinfo?(rob)

(In reply to Stuart Colville [:scolville] from comment #6)

What would be the effort in reliably detecting that a service worker was injected via an extension (assuming that's feasible)? Having SWs that were installed via an extension live and die with the extension sounds attractive.

In this particular case the extension is registering the service worker from a content script and intercepting the script url using the webRequest API,
but there is also another similar scenario:

an extensions may actually be intercepting a service worker that a website is registering on their own and changing the script by using the same strategy (the webRequest API).

I haven't actively looked into this yet (and so the details that follow may be incomplete or not completely true/doable) but maybe we could opt to make sure that we don't cache the service worker scripts that have been intercepted and changed by an extensions using the webRequest API, so that once the extension is uninstalled the script url would be loaded from the website site and not be the one that the extensions did change.

Let's also hear asuth opinion about this issue.

Flags: needinfo?(bugmail)

ServiceWorkers don't auto-unregister on 404's per spec decision: https://github.com/w3c/ServiceWorker/issues/204.

Persistent malicious ServiceWorkers are absolutely a concern, and I don't want to undercut that. In particular, mitigations are not something that would be obvious to sites unless they are actively checking their site logs or have been notified of attacks by bad actors.

In terms of mitigations from a site perspective:

  • Sites will inherently see the update checks and these will explicitly be labeled with a Service-Worker header.
  • A site can get all active registrations via navigator.serviceWorker.getRegistrations and unregister those that don't match those it currently expects. This does assume the site is able to get a clean load from the server, however. This will happen on shift-reload.

The primary intentional security invariants as they relate to malicious ServiceWorkers are that:

  • The root/main script must be same-origin to the site.
  • Fetches initiated by/on behalf of ServiceWorkers cannot be intercepted by the ServiceWorker. importScripts for explicitly sets the request's service-workers mode to "none".
  • And in general, the site wins over the ServiceWorker. There have been a number of design proposals to allow for ServiceWorkers that cryptographically validate their successor which are legitimate use-cases (ex: encrypted webmail), but unfortunately indistinguishable from the behavior a persistent compromised ServiceWorker would want.
  • The underlying script storage for a given ServiceWorker is never directly exposed to content.

:luca's proposal about marking specific ServiceWorker installs as tainted in the CS sense by extensions seems like one of the 2 feasible granularities. It's definitely the least disruptive mitigation from a user perspective as:

  • The user won't lose any data. ServiceWorker installs are silent and generally structured to be idempotent.
    • The caveat is that there's still only a single storage bucket used for all storage in the origin, and it's quite possible for a freshly installed ServiceWorker to be impacted by a previous ServiceWorker install.
  • There would be no ambiguity about the webextension having actively been involved in manipulating the ServiceWorker's installation. Actions taken by content scripts can do any number of complex things, but manipulating a SW install fetch is unambiguous.
  • The user intent of uninstalling an extension seems straightforward. The user expectation of uninstalling an extension is that it is no longer around and it will stop continuing to impact the browser. The ServiceWorker should be removed.

The other granularity is origin-level and tracking whether a webextension has potentially influenced the execution of script in an origin at all beyond rejecting loads. This would include:

  • Altering any network requests by performing a redirect or altering received content.
  • Use of content scripts or tabs.executeScript that allowed the extension principal to do anything under the guise of the origin in question. This includes creating script tags, direct manipulation of storage APIs, etc.

For this level of tainting, the user would potentially want to be offered the option to clear all cookies and site data for origins that had been impacted by the extension. The unfortunate reality is that malicious web extensions have a lot of options to manipulate an origin's browser storage that could continue to impact future browsing sessions. Various examples:

  • A freshly re-installed ServiceWorker may continue to use already populated Cache API storage thinking it's valid. Or the ServiceWorker may attempt to re-validate but be tripped up by data placed in storage maliciously and that the ServiceWorker doesn't know how to deal with. For example, an intentionally constructed IDB structured value, or an IDB database that has a much higher version than the ServiceWorker has used.
  • Sites like wikipedia have previously used LocalStorage as a JS script cache, which could be manipulated to contain attacker-controlled code.
  • Sites may have debugging/analytics infrastructure that's parameterized by data stored locally in the browser for A/B testing, statistical sampling, explicit support, or other possible reasons. These settings could be changed and send the data to an attacker-controlled origin. And as in the previous example, the attacker could leverage the general brittleness of most code to break self-correcting logic.

The user would need to be asked about wiping site data on uninstall, as the impact of wiping origin storage could be user noticeable. To this end, origin-level tainting might be combined with a prompting mechanism that grants permissions for an extension to access a given origin when the super-sketchy <all_urls> permission is used or when the "Access your data on # other sites" prompt makes it impossible to understand what permissions are being granted. This way the user is able to provide more informed consent on a per-site basis, and potentially understand the need to wipe the storage later. This could also provide somewhat of an audit log. It would be a general UX and security improvement, as it's unlikely that users implicitly understand that the "Access your data for all websites" grant means that any extension with the permission can have complete control over the user's banking/paypal/e-commerce sessions the next time they log in.

Flags: needinfo?(bugmail)

Another solution that may be worth considering is disallowing SW registration requests from being intercepted and/or modified by the webRequest API. Those are special types of requests and would simple enough to distinguish between normal HTTP requests.

I wouldn't be against preventing service worker access, but I'd like to have more data.

  • What does Chrome do in this case?
  • Can we identify how many extensions may be registering serviceworkers? (this would be hard to do for webRequest modifications)
  • Are there any valid use cases for extensions to install/modify service worker scripts?

Mitigations could be:

  • require a permission to do anything with service workers
    • and ensure removal on disable/uninstall of the extension
  • prevent any service worker access

(In reply to Shane Caraveo (:mixedpuppy) from comment #10)

I wouldn't be against preventing service worker access, but I'd like to have more data.

  • What does Chrome do in this case?

Chrome is not affected by this bug, as there is no webRequest.filterResponseData API that would allow modification of response bodies. The only way to change the response to the requested URL is through redirects, but that is not permitted either ("The script resource is behind a redirect, which is disallowed."). FYI Firefox also disallows redirected responses.

  • Can we identify how many extensions may be registering serviceworkers? (this would be hard to do for webRequest modifications)
  • Are there any valid use cases for extensions to install/modify service worker scripts?

Modify = blocking through webRequest? Yes, it's easy to imagine a valid use case - content blockers.
Modify = spoofing service worker scripts through webRequest? I can imagine use cases, but their existence do not justify the consequences of leaving this "feature" open by default. (emphasis on "by default" - opting in via a permission or pref seems more acceptable)

I think that a reasonable first step to resolve this bug is to prevent extensions from using webRequest.filterResponseData on service worker scripts, potentially behind a pref in case there is a legitimate need after all.

Flags: needinfo?(rob)

(In reply to Shane Caraveo (:mixedpuppy) from comment #10)

  • Are there any valid use cases for extensions to install/modify service worker scripts?

It seems likely to be necessary in the ad blocker arms race that ServiceWorkers would want to be modified, even if only for performance and simplicity. Plus, if ServiceWorkers became a way to avoid the reach of WebExtensions, I think it's likely we'd see them used inappropriately and all users would experience performance and power usage regressions that wouldn't help.

  • Can we identify how many extensions may be registering serviceworkers? (this would be hard to do for webRequest modifications)

In terms of installation, I think it's effectively impossible to tell if a content script is making a ServiceWorker install happen or not for halting problem reasons. Although I guess we could infer the less clever ones using whatever mechanism lets webextensions bypass CSP the page is subject to?

(In reply to Shane Caraveo (:mixedpuppy) from comment #10)

  • Can we identify how many extensions may be registering serviceworkers? (this would be hard to do for webRequest modifications)

I've filed a bug separately to cover the operational side of this - see bug 1637976 for details.

Severity: -- → S2
Priority: -- → P2

Ok, we'll prevent filtering serviceworker scripts with filterResponseData but have that behind a pref (turned on) so we can backout if necessaray.

Component: Untriaged → Request Handling
Flags: needinfo?(dveditz)

Removing employee no longer with company from CC list of private bugs.

Marking as confirmed since this bug is clearly valid.

Also linking to bug 1648635 because it is relevant.

Status: UNCONFIRMED → NEW
Ever confirmed: true
See Also: → 1648635
Flags: needinfo?(dveditz)
Keywords: sec-moderate
Summary: Extensions can Inject ServiceWorkers into Arbitrary Pages → Extensions can Inject ServiceWorkers which persist after uninstall
Duplicate of this bug: 1714220

We're going to introduce a pref to make Service Worker interception with filterResponseData an opt-in feature. We can explore other ways to resolve this issue (without disabling the feature) later. The scope of this bug is to resolve the service worker interception, we should also look into importScripts and maybe regular scripts too.

Bumping to P1 because we intend to resolve this in this version so that the next ESR (ESR91) includes this fix.

Assignee: nobody → rob
Status: NEW → ASSIGNED
Priority: P2 → P1
Assignee: rob → lgreco

Landed:
https://hg.mozilla.org/integration/autoland/rev/8ec39d08c9afc4c7eeef775ce7076bf6baa4b5b8
https://hg.mozilla.org/integration/autoland/rev/9791a5dc84d8eb1622d5ad8d6ad8bdbb3539f403

Backed out for causing xpcshell failures in test_ext_permissions_api.js:
https://hg.mozilla.org/integration/autoland/rev/a5bba1f25e5ab6085648c5906cecbfe9417e74e3

Push with failures: https://treeherder.mozilla.org/jobs?repo=autoland&group_state=expanded&resultStatus=usercancel%2Ctestfailed%2Cbusted%2Cexception&revision=9791a5dc84d8eb1622d5ad8d6ad8bdbb3539f403&selectedTaskRun=djZ-3WO5TiebkakaHjjpww.0
Failure log: https://treeherder.mozilla.org/logviewer?job_id=355972018&repo=autoland

[task 2021-10-25T18:36:56.372Z] 18:36:56     INFO -  TEST-PASS | xpcshell.ini:toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js | test_api_on_permissions_changed - [test_api_on_permissions_changed : 170] webRequest API is injected after permission request - Expected: true, Actual: true - true == true
[task 2021-10-25T18:36:56.372Z] 18:36:56  WARNING -  TEST-UNEXPECTED-FAIL | xpcshell.ini:toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js | test_api_on_permissions_changed - [test_api_on_permissions_changed : 170] webRequestFilterResponse.serviceWorkerScript API is injected after permission request - Expected: true, Actual: false - false == true
Flags: needinfo?(lgreco)

I have updated the patch that triggered this failure to make sure it does trigger that failure, and also confirmed with Shane that we are in agreement about pushing the updated patch back into autoland.

Flags: needinfo?(lgreco)
Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 95 Branch
Flags: qe-verify+
Whiteboard: [post-critsmash-triage]

Hello,

I’m attempting to verify the fix, however, I’m not entirely sure of the results I’ve obtained which should confirm the fix.

First I reproduced the issue on the latest Beta 94 (94.0/20211028161635) (which does not have the fix). I loaded the add-on in the first comment via about:debugging and indeed a new tab with https://example.com/ was opened. Returning to about:debugging as per the STR, a service worker with this name https://example.com/?injected-sw=1 is listed in the Service Workers section as Running. Removing the add-on, does not remove the service worker, however it will stop running after a short time - the status will say Stopped. I think this was the behavior the reporter of the bug noticed.

Then, on the latest Nightly 95 (95.0a1/20211031213949) (which has the fix), I did the same (loaded the add-on via about:debugging, example.com opened in a new tab, switched to about:debugging again). NO service worker was installed. The Service Worker section is empty.

Could you please confirm this is the expected result? I will not change the status of the issue until further notice.

Thank you !

Note: Tested on the latest Nightly and Beta under Windows 10 x64 and Ubuntu 16.04 LTS.

Attached image Beta.png
Attached image Nightly.png

Hi Luca, adding ni for Alex' question in comment 25. Thanks!

Flags: needinfo?(lgreco)

(In reply to Alex Cornestean from comment #25)

...
Could you please confirm this is the expected result? I will not change the status of the issue until further notice.
...

Hi Alex, I confirm that what you described in comment 25 is the expected result.

In particular:

  • on Firefox < 95: the attached test extension is able to register a fake ("non actually existing") serviceworker script on example.com by using:

    • blocking listener on webRequest.onHeaderReceived (used to set the content type that is expected to be valid for a service worker script)
    • blocking listener on webRequest.onBeforeRequest + webRequest.filterResponseData API method (used to set the body of the fake serviceworker script to the JS code the extensions wants to execute in the example.com service worker)
  • on Firefox >= 95: the attached test extensions fails to create the fake serviceworker script successfully because using webRequest.filterResponseData fails (as expected with the changes landed from this bug) and a TypeError: ServiceWorker script at https://example.com/?injected-sw=1 for scope https://example.com/ threw an exception during script evaluation. webRequest is logged in the browser console, as a side effect of the extension being unable to use webRequest.filterResponseData on the service worker script ([1]) .

Clearing a previously registered service worker isn't covered by any of the patches landed or attached to this bug as we previously agreed (see comment 14 and comment 18 for a short summary of what we agreed on for this bug).


[1]: if the extension does set a filter.onerror callback, Firefox is going to also call it with filter.error set to "Invalid Request ID", but that isn't the case for the attached test extension (anyway that behavior is covered by the attached test case, which has not been landed yet but it has been used locally to confirm the expected behaviors while working on the other 2 patches).

Flags: needinfo?(lgreco)

Thank you, Luca !

As per Comment 29, I will mark the issue as Verified Fixed.

Status: RESOLVED → VERIFIED
Whiteboard: [post-critsmash-triage] → [post-critsmash-triage][adv-main95+]
Attached file advisory.txt

Nice job, everyone! From my perspective that is fixed to me. The SW should never have been installed.

Alias: CVE-2021-43540
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.