28

Following the API documentation, I don't understand how to define a Content-Security-Policy HTTP Header for the renderer of my Electron application. I always get a warning in the DevTools.

I tried:

1) Copy/Paste the code in the API Doc, blindly:

app.on('ready', () => {
    const {session} = require('electron')
    session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
        callback({responseHeaders: `default-src 'self'`})
    })

    win = new BrowserWindow(...)
    win.loadUrl(...)
}

(By the way, I don't get why "Content-Security-Policy:" is missing in the string. But adding it don't change anything)

2) Modifying the session of the renderer with the same code:

win = new BrowserWindow(...)
win.loadUrl(...)

const ses = win.webContents.session;
ses.webRequest.onHeadersReceived((details, callback) => {
  callback({responseHeaders: `default-src 'self'`})
})

3) Add an extra header to ther renderer:

win = new BrowserWindow(...)
win.loadURL(`file://${__dirname}/renderer.html`,{
    extraHeaders: `Content-Security-Policy: default-src 'self'`
});

...

The only thing that works is using a meta tag in the renderer HTML file:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'>
phrogg
  • 888
  • 1
  • 13
  • 28
Anozer
  • 421
  • 1
  • 4
  • 12

5 Answers5

12

Not sure why the documentation contains this broken code. It confused the hell out of me but I found a working solution by trial and error:

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
    callback({ responseHeaders: Object.assign({
        "Content-Security-Policy": [ "default-src 'self'" ]
    }, details.responseHeaders)});
});

So the headers argument must be an object with the same structure as the original headers received in details.responseHeaders. And the original headers must be included in the passed object as well because this object seems to completely replace the original response headers.

The extraHeaders option isn't for response headers. It is for request headers sent to the server.

kayahr
  • 20,913
  • 29
  • 99
  • 147
  • 1
    Thanks for your answer. It makes perfect sense. Unfortunately it didn't work. There is no error but when I open the devtools in any renderer, there is a warning in the console about the absence of CSP (after removing the HTML tags). Am I missing something ? – Anozer Sep 10 '18 at 16:57
  • Yep. Unfortunately my page still happily loads any resource under the sun when I try to inject the headers this way. – Slbox Sep 11 '18 at 16:00
  • Using a debugger in main.js, I paused on a breakpoint in `onHeadersReceived()` to inspect `details.responseHeaders`. It seems that there is a default CSP with `"default-src 'none'"`. In both defaultSession and the session of my renderer. I don't understand why we can load external contents with this CSP. This is not working. – Anozer Sep 11 '18 at 17:28
  • The code works fine here. With `default-src 'none' ` my app no longer works because everything is blocked, with `self` it is working fine. Did you test loading stuff in the wevdev console? Maybe that is not affected by CSP. And this CSP warning of Electron is somewhat broken currently when context isolation is enabled. See https://github.com/electron/electron/issues/14510 – kayahr Sep 11 '18 at 18:30
  • My app still works with default-src 'none'. After more tests, it seems that `defaultSession.webRequest.onHeadersReceived` is only called by requests made after loading renderers. If I add `console.log(details.url)` just before `callback(...);`, the only URL printed in the console is an external request I coded in the renderer. Unlike `onBeforeRequest` that is realy executed each time a file is accessed (.css, .js, etc.). – Anozer Sep 15 '18 at 16:14
  • Is no way to make this a more complete answer? With the proper docs failing or at least misleading can this be cleared up? – shadowbq Sep 19 '18 at 00:03
  • Unfortunately, this didn't work for me. I was still able to access any external resource from the renderer processes. I set this both on session.defaultSession and in the individual renderer process's session. – Jared Sep 20 '18 at 18:04
  • Realized that some pages set it in lowercase like `content-security-policy`. Make sure to add that one as well to cover all cases. Also had to switch the Object.assign order of the objects. – matthiasgiger May 09 '21 at 19:49
1

If your aim is to be able to use CSP in both dev mode (with resources loaded by http:// protocol) and prod mode (file:// protocol) here's how you can do it:

First, remove the Content-Security-Policy meta from src/index.html - we need to inject it only for prod mode, because

  • onHeadersReceived will not work for file:// protocol as Electron docs confirm, and also because
  • if we keep it in src/index.html for Dev mode it will override the onHeadersReceived at least for part of resources, and for Dev mode we need different settings.

Then we can inject it for Prod mode with gulp-inject:

// in project dir
npm install --save-dev gulp-inject gulp
// src/index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <base href="">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- inject:prod-headers -->
  <!-- src/prod-headers.html content will be injected here -->
  <!-- endinject -->
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

// src/prod-headers.html
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
// gulpfile.js
var gulp = require('gulp');
var inject = require('gulp-inject');

gulp.task('insert-prod-headers', function () {
  return gulp.src('./dist/index.html')
    .pipe(inject(gulp.src('./src/prod-headers.html'), {
      starttag: '<!-- inject:prod-headers -->',
      transform: function (filePath, file) {
        // return file contents as string
        return file.contents.toString('utf8')
      }
    }))
    .pipe(gulp.dest('./dist'));
});

Then make sure npx gulp insert-prod-headers is run after e.g. ng build generates dist/index.html.

And for dev mode let's use onHeadersReceived similarly to Electron docs example:

const args = process.argv.slice(1);
const devMode = args.some((val) => val === '--serve');
app.on('ready', () => {
    if (devMode) {
      const {session} = require('electron')
      session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
        callback({responseHeaders: `default-src http: ws:`})
      })
    }

    win = new BrowserWindow(...)
    win.loadUrl(...)
}

This solution was tested on Electron 4.0.3.

Luke H
  • 3,125
  • 27
  • 31
Artem Vasiliev
  • 2,063
  • 1
  • 24
  • 21
  • When using the file:/// protocol, is there no way to have a dynamic csp per request? For instance, we have an internal api we hit after user is authenticate to get back the client's list of web apis that we then will hit throughout their session. We can't exactly do that during build time or even at startup – Aaron Vanderwielen Dec 11 '20 at 21:17
1

As pointed out in the Electron docs, you will have to use a Content Security Policy (CSP) Meta Tag in the html file when you load your renderer.html via file:// scheme (IIRC you do that in above example).

If you want to adjust the content security policy conditionally for prod and dev environment, you can dynamically generate this string inside the html in your build step. I suggest using a template engine like mustache.js (used in the example).

Example (file resources)

In my case I wanted to enable Hot Module Replacement (HMR) via websockets and file:// resource in dev mode, which required relaxing the CSP rules (but only in dev!).

index.mustache:

<html>
  <head>
    <meta
      http-equiv="Content-Security-Policy"
      content="{{{cspContent}}}"
    />
  </head>
...

cspContent.json for dev:

{
  "cspContent": "default-src 'self'; connect-src 'self' ws:"
}

build step in dev (for prod default values could be used):

npx mustache cspContent.json index.mustache > index.html

URL resources

For usage with URL resources, you can stick to this example:

const { session } = require('electron')

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ['default-src \'none\'']
    }
  })
})

Make sure to merge your custom CSP response headers with the default ones - you don't do that in your pasted example above. Here, you can also check conditionally for the environment.

Hope, it helps.

ford04
  • 66,267
  • 20
  • 199
  • 171
  • When using the file:/// protocol, is there no way to have a dynamic csp per request? For instance, we have an internal api we hit after user is authenticate to get back the client's list of web apis that we then will hit throughout their session. We can't exactly do that during build time or even at startup – Aaron Vanderwielen Dec 11 '20 at 21:30
  • At least Electron docs clearly state this: "CSP's preferred delivery mechanism is an HTTP header, however it is not possible to use this method when loading a resource using the `file://` protocol." - take a look at the first provided link above. – ford04 Dec 12 '20 at 09:09
  • Yes I have read that, hence my question. Wondering if anyone has had any luck trying something to allow a dynamic CSP, maybe even rewriting the actual index.html file's meta CSP tag before delivering a response to a request. – Aaron Vanderwielen Dec 14 '20 at 17:56
0

There isn't enough detail in your question to know whether you are having issues on initial load or subsequent web requests, but my issue was for the initial file load. With an Electron app using React, I was getting warnings about using inline scripts even with kayahr's code. This is because the onHeadersReceived method only catches requests that are made after the application has loaded initially. It will not stop any warnings from the initial application load.

I ended up having to use templating during my application build to add a nonce to the inline script and style and to the CSP header in the HTML file that the application loads initially.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-<%= scriptNonce %>'; style-src 'nonce-<%= styleNonce %>';">
    <link rel="stylesheet" type="text/css" href="./index.css" nonce=<%= styleNonce %>>
    <title>Basic Electron App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="application/javascript" nonce=<%= scriptNonce %>>
      require('./index.js');
    </script>
  </body>
</html>

index.css

body {
  margin: 0px;
}

.hello {
  font-family: "Century Gothic";
  width: 800px;
  margin: 70px auto;
  text-align: center;
}

and in gulfile.js add the following to what you already have and make sure this task is included in your pipeline. You can also just update your current html task with the code below.

const template = require('gulp-template');
const uuidv4 = require('uuid/v4');

gulp.task('copy-html', () => {
  // Create nonces during the build and pass them to the template for use with inline scripts and styles
  const nonceData = {
    scriptNonce: new Buffer(uuidv4()).toString('base64'),
    styleNonce: new Buffer(uuidv4()).toString('base64')
  };
  return gulp.src('src/*.html')
  .pipe(template(nonceData))
  .pipe(gulp.dest('dist/'));
});

This is a very stripped down example. I have a more complete example at https://github.com/NFabrizio/data-entry-electron-app if anyone is interested, though there is still one warning when running the application because one of the packages I am using pulls in react-beautiful-dnd, which adds inline styles but does not currently accept nonces.

NFab
  • 386
  • 4
  • 6
-1

Set the following meta tag in the renderers.

<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-xxx or sha256-yyy' " />

Kindly checkout my github repo electron-renderer-CSP-sample, containing samples for both nonce & SHA methods for internal & external js files as well.

Sudhakar Ramasamy
  • 1,736
  • 1
  • 8
  • 19