Proposal: blank worker · Issue #6911 · whatwg/html (original) (raw)

Blank Worker Explainer

Introduction

The web platform currently requires DedicatedWorker and SharedWorker scripts to be same-origin to the parent context creating them. This is largely motivated by the desire to avoid some of the issues associated with the creation of cross-origin iframes.

This restriction, however, creates a common headache for web developers. They often have scripts hosted on cross-origin CDNs. They cannot directly use these scripts to create a DedicatedWorker or SharedWorker. Instead they must use a workaround like:

const blob = new Blob(['importScripts("https://cdn.example/my/worker/script.js")'], { type: 'text/javascript' }); const blobURL = URL.createObjectURL(blob); const worker = new Worker(blob);

This works, but it is a persistent paper cut for web developers. It makes something that should be easy, complicated and non-obvious. It also risks leaking the blob URL if the code does not later call revokeObjectURL(). It also invokes a lot of complicated machinery in the browser to persist and load the blob. This overhead should not be necessary.

This effort proposes to improve the situation by providing two features that are available in iframes, but missing in DedicatedWorker and SharedWorker today:

  1. The ability to create a blank context.
  2. The ability to append scripts to an existing context.

With this proposal to provide these features, the example above could instead be written:

const worker = new Worker(); worker.executeScript('https://cdn.example/my/worker/script.js');

Goals

Non-Goals

Web APIs

This proposal includes two distinct API changes. In theory these are somewhat orthogonal, but we need both to address the motivating use case.

Blank Worker Construction

This API change simply provides a default constructor that has no script URL argument. So:

const w = new Worker(); const sw = new SharedWorker({ name: 'foo' });

Workers constructed in this way have a script URL of about:blankjs. The origin, policy container, service worker controller, etc of the owner are inherited by the worker context just as a child about:blank iframe inherits them from its parent. The about:blankjs resource will be considered to have an text/javascript mime type while about:blank has a text/html mime type.

Owner Initiated Script Execution

This API change proposes to allow the owning context to initiate script execution in the worker context.

const w = new Worker(); await w.importScripts(scriptURL);

This API could also support running modules:

const w = new Worker({ type: 'module' }); await w.addModule(scriptURL);

Alternatively we could instead expose a single w.executeScripts(url, { type }) method.

These methods would act as if they sent a postMessage() to the worker which then invoked importScripts() or addModule() in the worker context. It would then postMessage() back to the owning context, indicating that the script execution was completed. This would then resolve the promise returned from w.executeScripts().

Notably, this postMessage()-like behavior means that multiple calls to executeScript() would be queued. Modules that use top-level await could interleave, but otherwise all scripts would run in the order they were sent.

Considered Alternatives

The main alternative that is typically suggested is to simply allow new Worker() and new SharedWorker() to take cross-origin scripts. We don't want to do this for a couple of reasons.

First, we don't want to support cross-origin workers at the moment. We are still dealing with the long tail of consequences of allowing cross-origin iframes. If necessary, code can construct a cross-origin iframe which can then create its own worker.

Second, we don't want to support cross-origin scripts while keeping the worker same-origin to its owner because it would create a very exceptional loading situation. Today all contexts and javascript globals have an origin that matches the origin of their loading resource. Breaking this constraint would create an exceptional case in the browser which could lead to unexpected security issues.

Privacy & Security Considerations

This proposal does not store any user data or expose any information about the client to the server. It's mainly an ergonomic API change for something that is already achievable through the blob API. There should not be any privacy impact from this proposal.

In terms of security, however, there may be a few items to discuss.

First, it may be controversial to create a new special URL type like about:blankjs. One could argue we should instead use about:blank itself. That would be problematic, however, since about:blank has a text/html mime type. In addition, about:blank has numerous weird behaviors (initial about:blank, replacement, fragments, etc) that will not be supported in about:blankjs. We do not want to propagate these unusual features to workers and it would be another weirdness for about:blank to work inconsistently.

Second, it is possibly concerning that the owner can inject script into the worker at any time. This would be a new capability that existing scripts may not be expecting. We argue, however, that the owner/worker division is not a security boundary. The owner and worker already share storage, network cache, service workers, etc. There are many ways for the owner to attack the worker context if it wanted to.

In addition, it seems likely an owner could use blob URLs to construct the same behavior we are proposing here to inject script whenever it wants into a target worker thread, by executing a blob URL containing a script execution framework plus importScripts(originalURL), instead of by using new Worker(originalURL) directly. Same-origin scripts can potentially defend against this CSP, but again there are many other ways for the owner to attack the worker script via poisoned storage, cache, etc.

Acknowledgements

Thank you to @domenic and @surma for reviewing and contributing to this explainer.