Extensions can Inject ServiceWorkers which persist after uninstall
Categories
(WebExtensions :: Request Handling, defect, P1)
Tracking
(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
(Blocks 1 open bug)
Details
(Keywords: sec-moderate, Whiteboard: [post-critsmash-triage][adv-main95+])
Attachments
(7 files)
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.
Updated•5 years ago
|
Comment 1•5 years ago
|
||
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.
Comment 2•5 years ago
|
||
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.
Reporter | ||
Comment 3•5 years ago
|
||
(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 samescriptURL
.
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%A0I'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.
Comment 6•5 years ago
|
||
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.
Assignee | ||
Comment 7•5 years ago
|
||
(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.
Comment 8•5 years ago
|
||
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.- Sites can serve a self-unregistering ServiceWorker when this header is observed on unexpected URLs.
- Sites can serve a response containing a
Clear-Site-Data: storage
[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data).
- 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.
- Our implementation uses the Cache API in a chrome namespace.
- However, we do expose a Chrome-privileged API for devtools and tests. Devtools uses it here for content-only purposes.
: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.
Reporter | ||
Comment 9•5 years ago
|
||
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.
Comment 10•5 years ago
|
||
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
Comment 11•5 years ago
|
||
(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.
Comment 12•5 years ago
•
|
||
(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?
Comment 13•5 years ago
|
||
(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.
Updated•5 years ago
|
Comment 14•5 years ago
|
||
Ok, we'll prevent filtering serviceworker scripts with filterResponseData but have that behind a pref (turned on) so we can backout if necessaray.
Assignee | ||
Updated•5 years ago
|
Updated•5 years ago
|
Updated•5 years ago
|
Comment 15•4 years ago
|
||
Removing employee no longer with company from CC list of private bugs.
Comment 16•4 years ago
|
||
Marking as confirmed since this bug is clearly valid.
Also linking to bug 1648635 because it is relevant.
Updated•4 years ago
|
Comment 18•4 years ago
|
||
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.
Updated•3 years ago
|
Updated•3 years ago
|
Assignee | ||
Comment 19•3 years ago
|
||
Assignee | ||
Comment 20•3 years ago
|
||
Depends on D128736
Assignee | ||
Comment 21•3 years ago
|
||
Comment 22•3 years ago
|
||
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
Assignee | ||
Comment 23•3 years ago
|
||
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.
Comment 24•3 years ago
|
||
Log a more explicit error message on invalid redirects. r=asuth,mixedpuppy
https://hg.mozilla.org/integration/autoland/rev/7fd59a7f04059ee17ee40dfcfa2ce74b263dffd4
Permission check on stream filter parent creation. r=mixedpuppy
https://hg.mozilla.org/integration/autoland/rev/a5bee441d127717c8481c38b3d3db48511d6f5f7
https://hg.mozilla.org/mozilla-central/rev/a5bee441d127
https://hg.mozilla.org/mozilla-central/rev/7fd59a7f0405
Updated•3 years ago
|
Updated•3 years ago
|
Comment 25•3 years ago
|
||
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.
Comment 26•3 years ago
|
||
Comment 27•3 years ago
|
||
Comment 28•3 years ago
|
||
Hi Luca, adding ni for Alex' question in comment 25. Thanks!
Assignee | ||
Comment 29•3 years ago
|
||
(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)
- blocking listener on
-
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 aTypeError: 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).
Comment 30•3 years ago
|
||
Thank you, Luca !
As per Comment 29, I will mark the issue as Verified Fixed.
Updated•3 years ago
|
Updated•3 years ago
|
Comment 31•3 years ago
|
||
Reporter | ||
Comment 32•3 years ago
|
||
Nice job, everyone! From my perspective that is fixed to me. The SW should never have been installed.
Updated•3 years ago
|
Updated•2 years ago
|
Assignee | ||
Comment 33•2 years ago
|
||
Assigning needinfo to myself as a reminder for rebasing and landing the patch with the additional test case.
If Bug 1247687 did already land, consider adding also a test case with the test extension intercepting and trying to redirect main script and imported modules related to a { type: "module" }
service worker, otherwise to file a followup to track the additions to the test case.
Updated•2 years ago
|
Comment 34•2 years ago
|
||
Comment 35•2 years ago
|
||
bugherder |
Assignee | ||
Comment 36•2 years ago
|
||
(In reply to Luca Greco [:rpl] [:luca] [:lgreco] from comment #33)
Assigning needinfo to myself as a reminder for rebasing and landing the patch with the additional test case.
If Bug 1247687 did already land, consider adding also a test case with the test extension intercepting and trying to redirect main script and imported modules related to a
{ type: "module" }
service worker, otherwise to file a followup to track the additions to the test case.
Filed Bug 1807919 as a followup to cover the additional scenario in the test (landed in central in comment 35).
Description
•