This proposal aims to make it easy for JavaScript libraries to instantiate web workers when hosted on a CDN (or any origin that does not match) by introducing a executionOrigin
option to the Worker
constructor:
// https://web-app.example.com
import { calculation } from "https://cdn.example.com/lib/index.js";
console.log(await calculation());
// https://cdn.example.com/lib/index.js (served using CORS)
export async function calculation() {
const worker = new Worker(import.meta.resolve("./worker.js"), {
"type": "module",
"executionOrigin": "from-calling-script"
})
…
return …;
}
// https://cdn.example.com/lib/worker.js (served using CORS)
self.addEventListener("message", function (event) {
// heavy work can go here without freezing the main thread
});
Web workers are invaluable for implementing computationally intensive operations on the web, without blocking the main thread.
Unfortunately, writing portable web worker code has always been
difficult. This has gotten easier over time, due to new features like
import.meta.resolve(…)
. However, it remains difficult to instantiate a web worker using a URL that does
not share its origin with the calling script — the "CDN problem". This is because the default
execution origin of the a worker comes from its URL rather than the script that
is instantiating it, which blocks the worker code due the same-origin
policy.1
// Script on https://a.example.com
new Worker("https://b.example.com/worker.js"); // Throws `DOMException`
Nevertheless, it has long been possible to work around this by using a worker trampoline. That is to say, it is perfectly valid to instantiate a worker using the same origin as the calling script — this is already supported by browsers, and not a security issue. However, the worker trampoline is… not great:
- Although this trampoline is a combination of several straightforward web APIs, this combination is not obvious as a workaround to a web author encountering a
DOMException
. It's quite possible that most authors would assume it's impossible. - The implementation has footguns (read: security issues) if not carefully implemented, since it requires constructing a JavaScript source string.
- It requires loading the worker from a
blob:
URL, which in turn requires addingblob:
toworker-src
for a page with CSP. - The "obvious" implementation invites a memory leak in the form of an unrevoked object URL.
This is already sufficiently undesirable that most code authors don't ever prepare for their code to be hosted on a different origin than the page that uses it. But this presents a particular challenge when libraries hosted on CDNs, as these limitations are passed on to websites using these libraries and can ultimately result in a bad user experience when web workers fail to instantiate. (Note that it doesn't matter whether the worker instantiation happens from a script on the current page origin, or from a script on the CDN. The same problem applies in both cases.)
There are proposals that would provide flexible ergonomic APIs for working with web workers, each of which would also address this issue:
- Blank
Worker
- Module expressions (formerly "module blocks" in turn based on previous proposals)
Unfortunately, the scope of these proposals has prevented them from getting
close to shipping in any browsers. Therefore, this proposal focuses on the "CDN
use case" by adopting the simplest possible solution that has been discussed in
the blank Worker
proposal discussion: a way to specify a single script to be run in a worker while inheriting the execution origin of its calling script.
Add the executionOrigin
option to the Worker
constructor as follows (using TypeScript syntax):
declare class WorkerExecutionOriginPolyfill extends Worker {
constructor(
url: URL | string,
options?: {
// New option
executionOrigin?: "from-url" | "from-calling-script";
// Other options. Currently:
type?: "classic" | "module";
credentials?: "omit" | "same-origin" | "include";
},
);
}
When constructing a worker, if the executionOrigin
option is present and set to "from-calling-script"
:
- Set
QUOTED_URL
to a quoted JavaScript source form of a string containing the URL passed viaurl
.2 - If the
type
options is present and set to"module"
:- Set
SCRIPT_SOURCE
to the following:
- Set
import QUOTED_URL; // replace QUOTED_URL with the value from above
- Else:
- Set
SCRIPT_SOURCE
to the following:
- Set
importScripts(QUOTED_URL); // replace QUOTED_URL with the value from above
- Instead of instantiating the worker using the script content of
url
, set its script content to the value ofSCRIPT_SOURCE
. - The script URL and the CSP
worker-src
of theWorker
are both the string value ofurl
.
Note that it's not necessary for a browser to literally follow these steps, as long as semantically equivalent steps are substituted.
See:
WorkerExecutionOriginPolyfill.js
for a small, fully working working polyfill.- Note: this polyfill uses a worker trampoline, which has the drawbacks described in the "Motivation" section above. The main point of this proposal is for browsers to provide equivalent functionality without these drawbacks.
WorkerExecutionOriginPolyfill.d.ts
for TypeScript definition file matchin the proposed new form ofWorker
.
- If the blank
Worker
proposal is implemented, theexecutionOrigin
option could possibly be changed to be implemented or defined on top of it (although the top-level script URL of the web worker may be different ifabout:blankjs
is used). Either way, it should be possible to avoid any more security risk than the blankWorker
proposal by using a thoughtful implementation.- Also note that the
executionOrigin
option retains the single-URL constructor, which avoids the potential of injecting code into a worker that may not expect it.
- Also note that the
- Module expressions would probably make this proposal obsolete, but there is no inherent compatibility issue with supporting both.
Run make serve
in this repo and open http://localhost:8080
.
Footnotes
-
Note that this is in contrast with the
<script>
tag and imported scripts, which are run in the same origin as the calling script regardless of their URL. This relies on CORS as an alternative security mechanism. This proposal relies on CORS for security in exactly the same way. ↩ -
While a URL can't contain double quotes, note that it can contain single quotes, and a string value of
url
could contain single quotes. Expressed in JavaScript, safe ways to do this includeQUOTED_URL = JSON.stringify(url.toString());
orQUOTED_URL = `"${new URL(url)}"`;
. ↩