Skip to content
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

Registering service workers for unique origins? #1437

Open
mjbvz opened this issue Jun 19, 2019 · 9 comments
Open

Registering service workers for unique origins? #1437

mjbvz opened this issue Jun 19, 2019 · 9 comments

Comments

@mjbvz
Copy link

mjbvz commented Jun 19, 2019

Problem

I am looking for a way to register a service worker runs for a unique origin. The lifetime of the service worker would be tied to the lifetime of the page with that unique origin.

Details

I develop VS Code's webview API, which lets VS Code extensions render arbitrary html inside of the VS Code editor. I am currently trying to set up a sandboxed environment for webviews using iframes + service workers. Here's the basic structure:

<!-- Serve up from a unique origin so that the iframe cannot effect our main page -->
<iframe src="https://cdn.contoso.com" sandbox="allow-same-origin">
  <html>
     <body>

     <!--
        This is a static html page served up from cdn.contoso.com. It is where
        we have VS Code's scripts for managing webview content. 
     
        This is also where we register a service worker for cdn.contoso.com. We use 
        the service worker to implement a virtual `/vscode-resource/` endpoint
        that the untrusted html provided by extension can use to load resources
        from the user's workspace (we delegate the loading back to the main page,
        which lets us allow or deny this the request)
     -->
    <iframe width="100%" height="100%" sandbox="allow-same-origin">
        <!-- Actual html content coming from an extension -->

        <img src="/vscode-resource/Users/matt/workspace/cat.git"> // This goes through the service worker
     </iframe>
    </body>
  </html>
</webview>

The main issue with this approach is that all the webview content ends up being served from the same cdn.contoso.com origin, which means that webviews can end up effecting each other indirectly. I've tried various incantations of allow-same-origin and srcdoc and data uris, but have not been able to find a means to serve the iframe in a unique origin so that we can still register a service worker for it.

A secondary issue is that we need to stand up cdn.contoso.com to serve up the outer iframe content in the first place. This adds extra setup and maintenance cost for people hosting VS Code.

Here's the actual code that implements all this today:

Proposals

I believe that allowing service workers to be registered in sandboxed iframe that does not set allow-same-origin would solve the main issue for us. Since the origin of the page in that case is unique, my expectation is that the service worker's lifetime would be tied to that of the document itself. Once the document is unloaded, the browser could delete the service worker and clean up any related resources.

Another approach would be an API for registering a one-time service worker that runs on a unique origin. The page would then be able to use the unique origin to serve up content. The lifetime of the worker would be the same as the page itself (similar to URL.createObjectURL).

For example, this could look something like:

const worker = await navigator.createServiceWorkerEndpoint('source of my virtual server worker');

const iframe = document.createElement('iframe');
iframe.src = worker.url; // This will result in a `fetch` against our worker for `/` at our unique origin
document.body.appendChild(iframe);

The url would be a guaranteed unique origin, perhaps by using a unique identifier: sw-endpoint://00071f0e-fd75-4525-acde-cacb6fc7a1cd. The url would be usable on the page that created the worker as well as on any pages it embeds.

The registered service worker itself would behave just like a normal service worker. However its lifetime would be linked to the page that created it. This would also simplify the creation of a virtual server for us (because we do not use many parts of service workers, such as them being shared across multiple pages).

Possibly Related issues

@wanderview
Copy link
Member

This is an interesting use case. I think our original reason for blocking service workers on opaque origins is because at the time we didn't see how it could provide offline or another reasonable use case.

@asutherland
Copy link

URL.createObjectURL I think is generally considered a regrettable decision we wouldn't want to imitate. As designed it encourages leaking resources, and as browsers need to implement it (at least Firefox), it's actually even more horrible under the hood.

Being able to register a single ServiceWorker on a special scope that's explicitly for opaque origins for this exact scenario might work, though. Like: navigator.serviceWorker.register("/script.js", { scope: navigator.serviceWorker.opaqueOrigins }) where the sentinel scope requires that it would have been legal to specify a scope of "/".

@mjbvz
Copy link
Author

mjbvz commented Jul 10, 2019

That makes sense. I mainly brought up the createServiceWorkerEndpoint idea because it also would let us use a service worker to control the loading an iframe src (we can't use srcdoc directly because that causes the iframe to inherit the parent page's content security policy)

What are the next steps in continuing this discussion? I don't think our use case is specific to our implementation of webviews for VS Code

@mjbvz
Copy link
Author

mjbvz commented Jul 12, 2019

To try to workaround this limitation, I've setup a domain that serves the same content on every subdomain you access (this way, the served up pages have a unique origin). These subdomains are only used once by a client and a service worker is registered for them.

However I do not know if this will cause perf issue for browsers in practice. What will happen as the number of service workers builds up as we keep accessing new subdomains? I've asked about this on StackOverflow. I think I may be able to call unregister on the service worker inside window.onwillunload but am not sure, and I don't know if this is even something to worry about

@jakearchibald
Copy link
Contributor

Thoughts for TPAC:

  • Does this have a use outside of wrapped apps?
  • It feels like this has a lot of the same "opaque vs visible" difficulties as foreign fetch. Eg, if the untrusted content fetches something from the parent origin? Right now that will be seen as cross-origin, but with a service worker in the middle, controlled by the parent, it could be seen as a same origin response.
  • We'd need the concept of opaque-origin-but-created-from-non-opaque-content. Eg, we can't let evil.com create a service worker that picks up requests made by <iframe sandbox href="https://example.com">.

@Rainmen-xia
Copy link

mark, createServiceWorkerEndpoint . I need this api to create sw too.

@Syndace
Copy link

Syndace commented Jun 24, 2022

I'm in a similar position as mjbvz. I have same-origin, yet untrusted code, which I voluntarily sandbox. I would like the content to be controlled by my service worker. Additionally, I would like global key combinations such as Ctrl+S to work even when one of those iFrames has focus.

I have thought about serving the content from a different origin, then using allow-same-origin and two nested iFrames to set up the service worker and key event listeners, as described in more detail by mjbvz. However, my application is currently purely static, without any server-side infrastructure requirements, thus serving the content form (multiple) different origins would be an unfortunate new requirement.

I got the following idea to solve this: maybe opaque origins are too harsh, in that they fail same-origin checks even with themselves. If we had the ability to create unique origins that pass same-origin checks with themselves, we could use the nested iFrames approach without any server-side infrastructure.

This could e.g. be a new value for the sandbox attribute on iFrames:

<iframe sandbox="allow-scripts">
    <!-- Semantics unchanged, the content is placed into an opaque origin
         that fails same-origin checks even with itself. -->
</iframe>

<iframe sandbox="allow-scripts allow-unique-origin">
    <!-- The new option, which places the content into a unique origin,
         which is capable of passing same-origin checks with itself. -->
</iframe>

<iframe sandbox="allow-scripts allow-same-origin">
    <!-- Semantics unchanged, the content keeps its origin. -->
</iframe>

@ranma42
Copy link

ranma42 commented May 4, 2024

I got the following idea to solve this: maybe opaque origins are too harsh, in that they fail same-origin checks even with themselves. If we had the ability to create unique origins that pass same-origin checks with themselves, we could use the nested iFrames approach without any server-side infrastructure.

I might be misreading the specification, but AFAICT opaque origins should succeed the same-origin check exactly with themselves:

Two origins, A and B, are said to be same origin if the following algorithm returns true:

  1. If A and B are the same opaque origin, then return true.
  2. If A and B are both tuple origins and their schemes, hosts, and port are identical, then return true.
  3. Return false.

@ranma42
Copy link

ranma42 commented May 4, 2024

Thoughts for TPAC:

  • Does this have a use outside of wrapped apps?

I am hacking on a webapp to display archived webpages from MAFF or WARC files (so in a sense I would use to handle "wrapped webapps").
While many websites are obviously too dynamic to be archived, other are basically designed to allow for this (see PWAs).
OTOH I am currently facing some concerns regarding the best way to keep each of them isolated (not sharing localStorage, caches, ...).

  • It feels like this has a lot of the same "opaque vs visible" difficulties as foreign fetch. Eg, if the untrusted content fetches something from the parent origin? Right now that will be seen as cross-origin, but with a service worker in the middle, controlled by the parent, it could be seen as a same origin response.

In my use case, I would never perform a fetch from the service worker directly. Instead I would either:

  • generate data from within the SW (typically extracting the file from the bundle)
  • ask another context to provide data (for example by posting a message to a Client)
  • We'd need the concept of opaque-origin-but-created-from-non-opaque-content. Eg, we can't let evil.com create a service worker that picks up requests made by <iframe sandbox href="https://example.com">.

IIUC for the problem stated by the OP this might not even be needed, as the service worker they are registering is only supposed to handle requests made by <iframe sandbox href="your-opaque-origin-worker-url">. Conversely, requests from <iframe sandbox href="https://example.com"> would still be handled normally (by the https://example.com SW, if it exists).

Short-term, I am planning to try and (ab)use wildcard domain names to "generate" unique origins. This makes setting up the RPC between the SW and embedder non-trivial and requires an ad-hoc configuration on the server side.

What might be the best way to move forward with this issue?

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

7 participants