Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active July 10, 2023 14:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/da49b6c05aed48e6dfb28a2c7e87cf06 to your computer and use it in GitHub Desktop.
Save dfkaye/da49b6c05aed48e6dfb28a2c7e87cf06 to your computer and use it in GitHub Desktop.
Use a Worker() for eval() and Function() when Content-Security-Policy does not allow 'unsafe-eval'

'safe-eval' with Worker

Before we get started…

Prefer script-src-elem over script-src for better cross-browser support of this solution.

Update, May 13, 2022

I have a variation on this solution using embedded workers with blob URLs at https://gist.github.com/dfkaye/14e5cc5dbe5bb38a9d80f25f54061c7f.

The problem

You want to do this:

Function(`return ${expr}`)

But your Content-Security-Policy header prevents the main thread from running eval() or Function() by default; for example:

<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' 'strict-dynamic' 'nonce-4AEemGb0xJptoIGFP3Nd';"
>

Notably, the script-src policy does not contain 'unsafe-eval' — in effect, your site does not trust any code, whether yours or a third party's, to evaluate code safely.

The solution

  1. Create a web worker (we'll define its contents in a moment) and subscribe to its message event:
var worker = new Worker("./worker.js");

worker.addEventListener('message', function(e) {
  console.info('worker completed');

  // event.data contains the stringified JSON response
  var data = JSON.parse(e.data);

  // do things in the main thread using data...
  console.log(JSON.stringify(data, null, 2));
});
  1. Define the functionality you want inside the 'worker.js' file. The following pretends to assign a new value in data, specifically, data[cellid] = value, and then return the modified data, id, and value.
function evaluate(e) {
  console.info('worker evaluating');
  console.log(e.data);

var data = JSON.parse(e.data.data || '{}');
  var item = JSON.parse(e.data.item || '{}');
  var id = item.id;
  var value = item.value;

  // Replace cellId (A1, PP12, etc.) with data[cellid] value.
  var expr = value.substring(1).trim().replace(/[A-Z]+[\d]+/g, function(cellid) {
    var item = data[cellid] || { value: '' };
    var test = Number(item.value);

    // Quote the value if it can't be coerced to a Number.
    return test !== test ? '"' + item.value + '"' : (item.value || 0);
  });

  var computedValue = value;

  try {
    computedValue = Function(`return ${expr};`).call(null);
  } catch (e) {
    console.error({
      error: "Error evaluating " + value,
      expr,
      computedValue
    });
  }

  self.postMessage(JSON.stringify({
    id: id,
    value: computedValue
  }));
}
  1. Subscribe to the message event inside the worker
self.addEventListener('message', evaluate);
  1. Back in the main thread, create a function that calls worker.postMesage() — we'll assume the function uses fields from the data and item arguments, then makes that call:
function useWorker(data, item) {
  worker.postMessage({
    data: JSON.stringify(data),
    item: JSON.stringify(item)
  });
}
  1. Call that function
var data = {};
var item = { id: "test", value: "something" };

useWorker(data, item);

You should see the "worker completed" message in the console along with formatted JSON data:

{
  "id": "test",
  "value": "something"
}

Restrictions

  1. The worker must reside in its own file - it cannot be created from a Blob or data schema URI - more on that here → https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Content_security_policy.

  2. Content Security Policy's script-src must include 'self' or 'strict-dynamic' (recommend including both for browsers that do not support 'strict-dynamic') - more about the script-src directive here → https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src

Update 3 January 2020

  1. Do not use script-src — Use script-src-elem for best cross-browser support.

The script-src directive is not carefully supported across all browsers to enable workers launched from trusted scripts to run eval().

This past week (2020 new year's) I found that only Firefox <= 71 runs eval() in a worker launched from code in a script-src directive when defined in the <meta> tag in the page itself. Edge ignored it completely and ran the worker. Chrome complained that the worker's CSP did not allow 'unsafe-eval'.

Moving that directive to the server and adding the response header there resulted in ALL browsers failing to execute the worker.

Changing script-src to script-src-elem in both server and <meta> tag versions resulted in successful execution across all browsers.

  1. There is also a worker-src directive, but…

According to this, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src, you can specify a CSP header for worker scripts.

I had no success using this directive on server response headers or in the <meta> tag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment