diff --git a/lib/common/options.js b/lib/common/options.js
index 819d7a0e7e..7d6b952f4b 100755
--- a/lib/common/options.js
+++ b/lib/common/options.js
@@ -34,6 +34,9 @@ Options.from = function (_options) {
if (typeof options.extensions === 'string') {
options.extensions = [ options.extensions ]
}
+ if (options.render.csp === true) {
+ options.render.csp = { hashAlgorithm: 'sha256' }
+ }
const hasValue = v => typeof v === 'string' && v
options.rootDir = hasValue(options.rootDir) ? options.rootDir : process.cwd()
@@ -278,7 +281,8 @@ Options.defaults = {
},
etag: {
weak: false
- }
+ },
+ csp: undefined
},
watchers: {
webpack: {
diff --git a/lib/core/middleware/nuxt.js b/lib/core/middleware/nuxt.js
index 4e8ba2368b..76e343479e 100644
--- a/lib/core/middleware/nuxt.js
+++ b/lib/core/middleware/nuxt.js
@@ -11,7 +11,7 @@ module.exports = async function nuxtMiddleware(req, res, next) {
try {
const result = await this.renderRoute(req.url, context)
await this.nuxt.callHook('render:route', req.url, result)
- const { html, error, redirected, getPreloadFiles } = result
+ const { html, cspScriptSrcHashes, error, redirected, getPreloadFiles } = result
if (redirected) {
return html
@@ -61,6 +61,10 @@ module.exports = async function nuxtMiddleware(req, res, next) {
res.setHeader('Link', pushAssets.join(','))
}
+ if (this.options.render.csp) {
+ res.setHeader('Content-Security-Policy', `script-src 'self' ${(cspScriptSrcHashes || []).join(' ')}`)
+ }
+
// Send response
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html))
diff --git a/lib/core/renderer.js b/lib/core/renderer.js
index 834ad80f27..1eae2e9f48 100644
--- a/lib/core/renderer.js
+++ b/lib/core/renderer.js
@@ -9,6 +9,7 @@ const { createBundleRenderer } = require('vue-server-renderer')
const Debug = require('debug')
const connect = require('connect')
const launchMiddleware = require('launch-editor-middleware')
+const crypto = require('crypto')
const { setAnsiColors, isUrl, waitFor } = require('../common/utils')
const { Options } = require('../common')
@@ -315,7 +316,15 @@ module.exports = class Renderer {
HEAD += context.renderResourceHints()
}
- APP += ``
+ let serializedSession = `window.__NUXT__=${serialize(context.nuxt, { isJSON: true })};`
+ let cspScriptSrcHashes = []
+ if (this.options.render.csp) {
+ let hash = crypto.createHash(this.options.render.csp.hashAlgorithm)
+ hash.update(serializedSession)
+ cspScriptSrcHashes.push(`'${this.options.render.csp.hashAlgorithm}-${hash.digest('base64')}'`)
+ }
+
+ APP += ``
APP += context.renderScripts()
APP += m.script.text({ body: true })
@@ -331,6 +340,7 @@ module.exports = class Renderer {
return {
html,
+ cspScriptSrcHashes,
getPreloadFiles: context.getPreloadFiles,
error: context.nuxt.error,
redirected: context.redirected
diff --git a/test/basic.ssr.test.js b/test/basic.ssr.test.js
index c18aaf5708..33a4c94e1f 100755
--- a/test/basic.ssr.test.js
+++ b/test/basic.ssr.test.js
@@ -22,6 +22,9 @@ test.serial('Init Nuxt.js', async t => {
},
build: {
stats: false
+ },
+ render: {
+ csp: true
}
}
@@ -228,6 +231,12 @@ test('ETag Header', async t => {
t.is(error.statusCode, 304)
})
+test('Content-Security-Policy Header', async t => {
+ const { headers } = await rp(url('/stateless'), { resolveWithFullResponse: true })
+ // Verify functionality
+ t.is(headers['content-security-policy'], "script-src 'self' 'sha256-BBvfKxDOoRM/gnFwke9u60HBZX3HUss/0lSI1sBRvOU='")
+})
+
test('/_nuxt/server-bundle.json should return 404', async t => {
const err = await t.throws(rp(url('/_nuxt/server-bundle.json'), { resolveWithFullResponse: true }))
t.is(err.statusCode, 404)