Adding support for Content-Security-Policy script-src safe inline, for SSR state transfer

This commit is contained in:
Samuel Horwitz 2018-01-10 01:36:34 -05:00
parent c316be20ef
commit 6e9be715b2
4 changed files with 30 additions and 3 deletions

View File

@ -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: {

View File

@ -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))

View File

@ -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 += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })};</script>`
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 += `<script type="text/javascript">${serializedSession}</script>`
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

View File

@ -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)