How to fetch JavaScript from server, track download progress, and not use `unsafe-eval` in Content-Security-Policy?

Viewed 349

I have a heavy JavaScript file on the server (> 3MB). I want to load the page fast and show a loading progress bar to the user. Currently, I am using fetch and WritableStream to download the data and to track the download progress as:

let resource = await fetch('heavy_file.js')
resource.clone().body.pipeTo(new WritableStream({
    write(t) { on_receive(t.length) }
}))

And then I am using the Function to evaluate it. This has several problems. How can I:

  • Load the script preferably using fetch (I'm using the same method to load WASM files, I want to track their download progress as well, and the WebAssembly.compileStreaming API requires the usage of fetch).
  • Track the download progress in a way that would work across modern browsers nowadays.
  • Be able to use this solution without enabling script-src 'unsafe-eval' in Content-Security-Policy?

PS. Of course, currently, we need to use script-src 'wasm-eval' in Chrome when loading WASM files until the bug is fixed.

1 Answers

Option 1

If you can calculate the hash of the script you're loading in advance (if it's not something generated dynamically), then a simple option to avoid

enabling script-src 'unsafe-eval' in Content-Security-Policy

would be to add only a hash of that specific script into the CSP header - this will still ensure that you're not executing any untrusted code, while allowing you to load and execute script manually.

MDN has some more examples of implementing such CSP policies here.

As for loading itself, you have two different paths from here:

Option 1.1

Combine the hash with a CSP3 policy unsafe-hashes which will allow you to keep using Function or eval like you currently, while still limiting code only to trusted.

For example, if you have a script like

alert('Hello, world.');

then your CSP header should contain

Content-Security-Policy: script-src 'unsafe-hashes' 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

Unfortunately, CSP level 3, or, at least, this option is supported only in Chromium at the moment of writing.

Option 1.2

Instead of using Function or eval, you can dynamically create a script tag from JavaScript, populate its textContent with your response content and inject it into the DOM:

let resource = await fetch('heavy_file.js')
resource.clone().body.pipeTo(new WritableStream({
    write(t) { on_receive(t.length) }
}));
resource.text().then(res => {
  let s = document.createElement('script');
  s.textContent = res;
  document.head.appendChild(s);
});

In this case you only need to add the hash of the script to the CSP and it will work across all browsers:

Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

Option 2

If you do want to support dynamically generated scripts, then your only option might be to move your progress-tracking code to a Service Worker, and then use Client.postMessage to communicate progress to a script on the page.

This will work for any content served from your origin, but only once Service Worker is installed - usually on a subsequent page load, and so might not help you if the large script you're loading is part of the page user visits first on your website.

Related