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') { if (typeof options.extensions === 'string') {
options.extensions = [ options.extensions ] options.extensions = [ options.extensions ]
} }
if (options.render.csp === true) {
options.render.csp = { hashAlgorithm: 'sha256' }
}
const hasValue = v => typeof v === 'string' && v const hasValue = v => typeof v === 'string' && v
options.rootDir = hasValue(options.rootDir) ? options.rootDir : process.cwd() options.rootDir = hasValue(options.rootDir) ? options.rootDir : process.cwd()
@ -278,7 +281,8 @@ Options.defaults = {
}, },
etag: { etag: {
weak: false weak: false
} },
csp: undefined
}, },
watchers: { watchers: {
webpack: { webpack: {

View File

@ -11,7 +11,7 @@ module.exports = async function nuxtMiddleware(req, res, next) {
try { try {
const result = await this.renderRoute(req.url, context) const result = await this.renderRoute(req.url, context)
await this.nuxt.callHook('render:route', req.url, result) await this.nuxt.callHook('render:route', req.url, result)
const { html, error, redirected, getPreloadFiles } = result const { html, cspScriptSrcHashes, error, redirected, getPreloadFiles } = result
if (redirected) { if (redirected) {
return html return html
@ -61,6 +61,10 @@ module.exports = async function nuxtMiddleware(req, res, next) {
res.setHeader('Link', pushAssets.join(',')) res.setHeader('Link', pushAssets.join(','))
} }
if (this.options.render.csp) {
res.setHeader('Content-Security-Policy', `script-src 'self' ${(cspScriptSrcHashes || []).join(' ')}`)
}
// Send response // Send response
res.setHeader('Content-Type', 'text/html; charset=utf-8') res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html)) res.setHeader('Content-Length', Buffer.byteLength(html))

View File

@ -9,6 +9,7 @@ const { createBundleRenderer } = require('vue-server-renderer')
const Debug = require('debug') const Debug = require('debug')
const connect = require('connect') const connect = require('connect')
const launchMiddleware = require('launch-editor-middleware') const launchMiddleware = require('launch-editor-middleware')
const crypto = require('crypto')
const { setAnsiColors, isUrl, waitFor } = require('../common/utils') const { setAnsiColors, isUrl, waitFor } = require('../common/utils')
const { Options } = require('../common') const { Options } = require('../common')
@ -315,7 +316,15 @@ module.exports = class Renderer {
HEAD += context.renderResourceHints() 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 += context.renderScripts()
APP += m.script.text({ body: true }) APP += m.script.text({ body: true })
@ -331,6 +340,7 @@ module.exports = class Renderer {
return { return {
html, html,
cspScriptSrcHashes,
getPreloadFiles: context.getPreloadFiles, getPreloadFiles: context.getPreloadFiles,
error: context.nuxt.error, error: context.nuxt.error,
redirected: context.redirected redirected: context.redirected

View File

@ -22,6 +22,9 @@ test.serial('Init Nuxt.js', async t => {
}, },
build: { build: {
stats: false stats: false
},
render: {
csp: true
} }
} }
@ -228,6 +231,12 @@ test('ETag Header', async t => {
t.is(error.statusCode, 304) 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 => { test('/_nuxt/server-bundle.json should return 404', async t => {
const err = await t.throws(rp(url('/_nuxt/server-bundle.json'), { resolveWithFullResponse: true })) const err = await t.throws(rp(url('/_nuxt/server-bundle.json'), { resolveWithFullResponse: true }))
t.is(err.statusCode, 404) t.is(err.statusCode, 404)