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:
- The ability to create a blank context.
- 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
- Provide a more convenient way to construct
DedicatedWorker
andSharedWorker
threads using cross-origin scripts. - Allow more easily running multiple scripts in a single worker, from the "outside" (without needing a custom postMessage signaling solution)
- Better align worker and iframe behavior and infrastructure.
Non-Goals
- This effort is explicitly not interested in creating cross-origin
DedicatedWorker
orSharedWorker
contexts where theirself.origin
differs from their owner'sself.origin
.
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.