Skip to content

Add a Service-Worker-Exclude Header #1690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Daniel-Abrecht opened this issue Aug 19, 2023 · 8 comments
Open

Add a Service-Worker-Exclude Header #1690

Daniel-Abrecht opened this issue Aug 19, 2023 · 8 comments

Comments

@Daniel-Abrecht
Copy link

I'm currently developing an SSO solution, and ran into some difficulties: https://github.com/Daniel-Abrecht/dpa-sso#security-relevant-limitations

To sum it up, I can't rely on cross origin cookies to store a session token, because browsers allow disabling them nowadays. For that reason, I pass it to a well known location at the origin which needs the token as a get parameter. If that origin has a service worker installed, it could intercept that token. For some applications, that may be desirable, but for others, it could be problematic.

I'm still looking for a safer way to pass the token, but it would be nice to also have a way to prevent a service worker from handling requests to certain locations too.

For that reason, I propose adding a Service-Worker-Exclude header, that would have some similarities to the Service-Worker-Allowed header. I would like it to work as follows. Service-Worker-Exclude should contain a list of locations who will not be handled by the service worker and just bypass it entirely. It should be set when installing the service worker. That way, I could, for example, make sure the entire /.well-known/ directory won't be handled by a service worker.

@bathos
Copy link

bathos commented Aug 20, 2023

A service worker is controlled by the same origin that you’re sending the request to. What’s being imagined here, exactly? It seems considerably more shaky at a glance to me to suggest requests from a different origin should be able to choose to bypass internal aspects of that origin’s own implementation of its endpoints.

@Daniel-Abrecht
Copy link
Author

That's not what I'm proposing. I'm proposing that, when an origin installs it's worker, a Service-Worker-Exclude header can be provided to exclude certain locations on it's own origin from being handled by it.
My understanding is, that it already doesn't matter where a request came from, but only where it goes to, for which service worker is going to handle it, and that the Service-Worker-Allowed header already operates in a similar way regarding when it needs to be set.
My proposal doesn't change any of that. It just allows to make sure some locations won't be handled by a service worker.

I do have a use-case where this would be useful, by allowing me to make sure the client side of my application won't be able to intercept a request to a specific location on my applications origin. But that is just a use-case, something this would be useful for, not the proposal itself.

@yoavweiss
Copy link

yoavweiss commented Jan 24, 2025

Thanks for filing this!

I have an extremely similar use case, where I need to be able to exclude certain paths from being handled by the service worker.

When chatting through this on Chromium's worker-dev it was mentioned that the static routing API can handle this use case.
They do not address my use case for the following reasons:

Specificity

It's not immediately obvious to me what happens with

addEventListener('install', (event) => {
  event.addRoutes({
      condition: {
        urlPattern: new URLPattern({pathname: "/.well-known/*"})
      },
      source: "network"
    });
}

addEventListener('install', (event) => {
  event.addRoutes({
      condition: {
        urlPattern: new URLPattern({pathname: "/.well-known/secret/*"})
      },
      source: "fetch-event"
    });
}

Would the route added in the second event be ignored?

Subresource requests

My use case is about excluding the environment from the SW scope, not just the request itself.
WICG/service-worker-static-routing-api#7 discusses this, but it seems like a separate primitive to enable this would be simpler.

Deployability

Having to programmatically call addRoute from the SW's install event adds a lot of complexity for a platform provider that wants to enable developers to install limited service workers on their origin. A header is significantly easier to deploy.

/cc @yoshisatoyanagisawa @sisidovski @domenic

@yoshisatoyanagisawa
Copy link
Collaborator

Would the route added in the second event be ignored?

For that case, I think the second event will be ignored because the rule will be added upon addRoutes() and evaluated from the beginning. Since a path "/.well-known/secret/" should be covered by "/.well-known/", the first rule is matched and executed, and the second rule won't be evaluated.

Let me go back to the original proposal on the Service-Worker-Exclude header.

Please correct if I understand wrong but I feel Service-Worker-Exclude is not oppose concept to Service-Worker-Allowed. As far as I read the examples in the specification (https://w3c.github.io/ServiceWorker/#service-worker-script-response) and Chromium code (https://source.chromium.org/chromium/chromium/src/+/main:content/browser/service_worker/service_worker_new_script_loader.cc;l=303;drc=27d34700b83f381c62e3a348de2e6dfdc08364b8), it relax or restrict the scope option given by the register() on register()'s evaluation. The scope given via the register() option should be used for matching during the navigation. If we follow the concept, Service-Worker-Exclude should be used for restricting the scope option given via the register(), and evaluated at that time. Therefore, I feel the name misleading.

To realize the expectations, I suppose there are three ways:

  1. expand the scope option in the register() to allow it have excluded paths.
  2. expand the ServiceWorker static routing API to follow subsequent subresource load (as discussed in Subsequent subresource requests' sources follow the navigation request's source. WICG/service-worker-static-routing-api#7)
  3. new header given with the ServiceWorker main script to mention what paths should be excluded from the ServiceWorker scope. (Yoav might prefer this way upon Add a Service-Worker-Exclude Header #1690 (comment))

I feel 1. is straight forward to realize the concept, but it brings scope matching more complex.
For 2., it is known that it currently needs another syntax for supporting subsequent subresource requests. However, the area is actively evolving and may easy to add a new syntax there.
For 3., a platform provider may prefer this way, but it also adds yet another place users need to care if the ServiceWorker is not applied as expected. Unlike Javascript APIs, finding invalid rules can be cumbersome.

I am leaning on 2., but I would like to listen to how others think on this.

@domenic
Copy link
Contributor

domenic commented Jan 27, 2025

  1. expand the scope option in the register() to allow it have excluded paths.

I think this was one of the original use cases for URLPattern: https://github.com/whatwg/urlpattern/blob/main/explainer.md

@sisidovski
Copy link
Contributor

Just to clarify, Service-Worker-Exclude is the response header that is added to the response of the service worker script resource, right?

Conceptually we have two options I think. Making some resources out of SW control 1) at the scope level or 2) via Static Routing API.

In each option, we have to store the information that this resource is excluded somewhere e.g. database, and use it in the next navigation. So essentially those are not so different I guess.

IMHO, I feel it's better not to expose the header to exclude some paths in order to keep the API simple. Unlike Service-Worker-Allowed, Service-Worker-Exclude defines the scope by itself. But the scope is defined in the client side JavaScript now. The header based scope definition may bring new complexity.

I'm inclined to 1 or 2, but 2 sounds more easier to achieve because this can be achieved as the extension of the existing static routing infra from both standardization and implementation wise.

@Daniel-Abrecht
Copy link
Author

Daniel-Abrecht commented Feb 4, 2025

I don't care all that much how it'll work in the end. However, I proposed a header because the server delivering the site will be in control of setting them, and the browser can enforce them. The scripts run on the site can then not circumvent that.
I don't think having it controlled on the client side by a script will work for me. After all, my use case is so that the site can't install a service worker and use it to get the token sent to the well known location. If the exclusion is set by a script on the client side, then such a script could also simply not set that exclusion, circumventing the protection it was meant to provide.

Maybe another idea would be to just define a fixed location that can never be handled by a service worker. If it was defined that, for example, /.well-known/secret/* was never handled by a worker, then I could simply put an endpoints there when I want to be sure of that.

@yoshisatoyanagisawa
Copy link
Collaborator

I'm revisiting my earlier comment in #1690 (comment) in light of our evolving understanding of the threat model. Originally, I had assumed a scenario where the server, ServiceWorker script, and the registering page were under the same ownership or at least trusted parties. I now understand that we need to account for a more hostile environment where each component could be adversarial. This has clarified why the header approach is being considered.

The use of headers brings Content Security Policy to mind. Since script injection is a primary concern, I would like to understand why using a <script> tag with a nonce for register() would not be a viable option in this hostile context. Could you share some insights into the limitations of this approach in this scenario?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants