Nonce property for inline scripts? (CSP)

Hi,

Has anyone tried to add a nonce property to inline scripts created and served by a meteor app to get to a state where unsafe-inline can be purged from the script-src part of the CSP (Content Security Policy)?

It would need to be generated for each http request and inserted as an attribute to all inline scripts.
I saw that webpack has some support for this, but I’m using the built-in Meteor packaging.

1 Like

I am working on this exact task right now for one of our Meteor apps.

In production mode, the bundler concatenates almost all client-side javascript in the application into a single minified javascript include file which does not need a nonce or sha-256 hash.

The only block that needs a sha-256 hash is the inline script that sets the variable meteor_runtime_config.

You can use the following line of code on the server side to calculate the sha-256 hash of that variable and use it to configure the CSP script-src header:

runtimeConfigHash = crypto.createHash('sha256').update(`__meteor_runtime_config__ = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(__meteor_runtime_config__))}"))`).digest('base64');

Most people use the browser-policy atmosphere package or node.js helmet package for generating CSP headers in their Meteor webapp.

Unfortunately we still don’t have a solution to avoid the need for ‘unsafe-inline’ in the CSP style-src header because most apps need to dynamically modify the DOM.

To solve this problem, Meteor would need a special function or other mechanism to generate HTML style blocks that contain a nonce.

Thanks for the pointers.

I use my own code to set CSP headers since meta attributes using helmet don’t cover all settings, using a line of code copied from the browser-policy package and then I have some conditionals of my own depending on Meteor.isDevelopment and some s3 storage bucket urls used to serve images.

import { WebApp } from "meteor/webapp";
import { NextHandleFunction } from "connect";

const cspHandler: NextHandleFunction = function(req, res, next) {
  const s3StorageUrl = getStorageBaseUrl();
  const webSockUrls = Meteor.isDevelopment
    ? "ws://localhost:3000 wss://redacted.ngrok.io"
    : "wss://test.redacted wss://app.redacted";

  const contentSecurityPolicy = `
    default-src 'none';
    img-src 'self' data: blob: https://storage.googleapis.com ${s3StorageUrl} https://media.licdn.com  https://www.google-analytics.com  https://stats.g.doubleclick.net;
    script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://www.gstatic.com;
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com;
    connect-src 'self' ${webSockUrls} https://api.mixpanel.com  https://www.google-analytics.com  https://stats.g.doubleclick.net https://engine.montiapm.com/;
    font-src 'self' data: https://fonts.gstatic.com;
    frame-ancestors 'none';
    base-uri https://test.redacted https://app.redacted;
    report-uri https://redacted.report-uri.com/x/y/enforce;
  `.replace(/\n/g, " "); // Headers cannot contain newlines
  res.setHeader("Content-Security-Policy", contentSecurityPolicy);
  res.setHeader("Referrer-Policy", "no-referrer");
  res.setHeader("X-Content-Type-Options", "nosniff");
  if (req.headers["x-forwarded-proto"] === "https") {
    res.setHeader("strict-transport-security", "max-age=31536000");
  }
  next();
};

export const installCSPHook = () => {
  // Copied from https://github.com/meteor/meteor/blob/devel/packages/browser-policy-common/browser-policy-common.js
  WebApp.rawConnectHandlers.use(cspHandler);
};

Ideally I’d like to have CSP turned on also in development mode for testing so then I’d want to insert a nonce for the uncompiled scripts.

1 Like

And as far as style-src goes, I think I’m toast since I’m using React anyway :slight_smile:

A small tweak to my previous code snippet. The client-side value of meter_runtime_config has an additional value “isModern”: true appended to the end, so this code incorporates that to ensure the correct sha-256 hash is calculated.

const runtimeConfigClient = Object.assign(__meteor_runtime_config__, {"isModern": true});
const runtimeConfigHash = crypto.createHash('sha256').update(`__meteor_runtime_config__ = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(runtimeConfigClient))}"))`).d
igest('base64');

I think that somehow extending/patching/overriding the internal Boilerplate package would be the best solution (that’s where the inline script code is generated).

Unfortunately the code doesn’t look very patchable to me…

Unfortunately further testing showed that this approach was not reliable. The variable meteor_runtime_config changes based on the type of client (i.e. browser version, desktop/mobile).

I have created an issue on Github suggesting a robust approach to fix this.

Turns out that support for loading the variable meteor_runtime_config from a separate .js file meteor_runtime_config.js was there all along, but not documented.

In your source code, you need to add the following include line:

import { WebAppInternals } from "meteor/webapp";

Then in a Meteor.startup() block, you need to add

WebAppInternals.setInlineScriptsAllowed(false);

Once you’ve done this, the variable meteor_runtime_config will no longer be loaded inline and you can remove the CSP policy ‘unsafe-inline’ from your script-src setting.

4 Likes

For the record, I want to clarify that if you are using the browser-policy package, you do not need to call WebAppInternals.setInlineScriptsAllowed(false).

Instead, call BrowserPolicy.content.disallowInlineScripts(), which internally invokes the above function.

The reason why we don’t use browser-policy in our Meteor webapps is because we are using Nginx to terminate the connection and we are setting our HTTP response headers there.

We also feel this is more secure as it avoids the risk of someone accidentally or maliciously disabling or weakening the Content Security Policy by tampering with the source code of our Meteor app.

1 Like

Not sure where you found this, but it seems very important. Thanks!

1 Like