diff --git a/examples/with-strict-csp-hash/README.md b/examples/with-strict-csp-hash/README.md
new file mode 100644
index 00000000..21b5e667
--- /dev/null
+++ b/examples/with-strict-csp-hash/README.md
@@ -0,0 +1,48 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-strict-csp-hash)
+
+# Example app with strict CSP generating script hash
+
+## How to use
+
+### Using `create-next-app`
+
+Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
+
+```bash
+npx create-next-app --example with-strict-csp-hash with-strict-csp-hash-app
+# or
+yarn create next-app --example with-strict-csp-hash with-strict-csp-hash-app
+```
+
+### Download manually
+
+Download the example:
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-strict-csp-hash
+cd with-strict-csp-hash
+```
+
+Install it and run:
+
+```bash
+npm install
+npm run dev
+# or
+yarn
+yarn dev
+```
+
+Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
+
+```bash
+now
+```
+
+## The idea behind the example
+
+This example features how you can set up a strict CSP for your pages whitelisting next's inline bootstrap script by hash.
+In contrast to the example `with-strict-csp` based on nonces, this way doesn't require running a server to generate fresh nonce values on every document request.
+It defines the CSP by document `meta` tag.
+
+Note: There are still valid cases for using a nonce in case you need to inline scripts or styles for which calculating a hash is not feasible.
diff --git a/examples/with-strict-csp-hash/package.json b/examples/with-strict-csp-hash/package.json
new file mode 100644
index 00000000..bcb456b4
--- /dev/null
+++ b/examples/with-strict-csp-hash/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "with-strict-csp-hash",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0"
+ },
+ "license": "ISC"
+}
diff --git a/examples/with-strict-csp-hash/pages/_document.js b/examples/with-strict-csp-hash/pages/_document.js
new file mode 100644
index 00000000..a894ff12
--- /dev/null
+++ b/examples/with-strict-csp-hash/pages/_document.js
@@ -0,0 +1,26 @@
+import crypto from 'crypto'
+import Document, { Head, Main, NextScript } from 'next/document'
+
+const cspHashOf = (text) => {
+ const hash = crypto.createHash('sha256')
+ hash.update(text)
+ return `'sha256-${hash.digest('base64')}'`
+}
+
+export default class extends Document {
+ render () {
+ const csp = `default-src 'self'; script-src 'self' ${cspHashOf(NextScript.getInlineScriptSource(this.props))}`
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/examples/with-strict-csp-hash/pages/index.js b/examples/with-strict-csp-hash/pages/index.js
new file mode 100644
index 00000000..3d446a4e
--- /dev/null
+++ b/examples/with-strict-csp-hash/pages/index.js
@@ -0,0 +1,3 @@
+export default () => (
+ Hello World
+)
diff --git a/server/document.js b/server/document.js
index 49dc22f0..361bb437 100644
--- a/server/document.js
+++ b/server/document.js
@@ -176,6 +176,28 @@ export class NextScript extends Component {
})
}
+ static getInlineScriptSource (documentProps) {
+ const { __NEXT_DATA__ } = documentProps
+ const { page, pathname } = __NEXT_DATA__
+
+ return `
+ __NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}
+ module={}
+ __NEXT_LOADED_PAGES__ = []
+
+ __NEXT_REGISTER_PAGE = function (route, fn) {
+ __NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
+ }${page === '_error' ? `
+
+ __NEXT_REGISTER_PAGE(${htmlescape(pathname)}, function() {
+ var error = new Error('Page does not exist: ${htmlescape(pathname)}')
+ error.statusCode = 404
+
+ return { error: error }
+ })`: ''}
+ `
+ }
+
render () {
const { staticMarkup, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
const { page, pathname, buildId } = __NEXT_DATA__
@@ -183,22 +205,7 @@ export class NextScript extends Component {
return
{staticMarkup ? null : }
{page !== '/_error' && }
diff --git a/test/integration/app-document/pages/_document.js b/test/integration/app-document/pages/_document.js
index e9ca56cc..aa64345a 100644
--- a/test/integration/app-document/pages/_document.js
+++ b/test/integration/app-document/pages/_document.js
@@ -1,5 +1,12 @@
+import crypto from 'crypto'
import Document, { Head, Main, NextScript } from 'next/document'
+const cspHashOf = (text) => {
+ const hash = crypto.createHash('sha256')
+ hash.update(text)
+ return `'sha256-${hash.digest('base64')}'`
+}
+
export default class MyDocument extends Document {
static async getInitialProps (ctx) {
let options
@@ -21,13 +28,24 @@ export default class MyDocument extends Document {
const result = ctx.renderPage(options)
- return { ...result, customProperty: 'Hello Document' }
+ return { ...result, customProperty: 'Hello Document', withCSP: ctx.query.withCSP }
}
render () {
+ let csp
+ switch (this.props.withCSP) {
+ case 'hash':
+ csp = `default-src 'self'; script-src 'self' ${cspHashOf(NextScript.getInlineScriptSource(this.props))}; style-src 'self' 'unsafe-inline'`
+ break
+ case 'nonce':
+ csp = `default-src 'self'; script-src 'self' 'nonce-test-nonce'; style-src 'self' 'unsafe-inline'`
+ break
+ }
+
return (
+ {csp ? : null}
diff --git a/test/integration/app-document/test/csp.js b/test/integration/app-document/test/csp.js
new file mode 100644
index 00000000..f468eeb9
--- /dev/null
+++ b/test/integration/app-document/test/csp.js
@@ -0,0 +1,21 @@
+/* global describe, it, expect */
+
+import webdriver from 'next-webdriver'
+
+export default (context, render) => {
+ describe('With CSP enabled', () => {
+ it('should load inline script by hash', async () => {
+ const browser = await webdriver(context.appPort, '/?withCSP=hash')
+ const errLog = await browser.log('browser')
+ expect(errLog.filter((e) => e.source === 'security')).toEqual([])
+ browser.close()
+ })
+
+ it('should load inline script by nonce', async () => {
+ const browser = await webdriver(context.appPort, '/?withCSP=nonce')
+ const errLog = await browser.log('browser')
+ expect(errLog.filter((e) => e.source === 'security')).toEqual([])
+ browser.close()
+ })
+ })
+}
diff --git a/test/integration/app-document/test/index.test.js b/test/integration/app-document/test/index.test.js
index a073a191..5febe4d7 100644
--- a/test/integration/app-document/test/index.test.js
+++ b/test/integration/app-document/test/index.test.js
@@ -12,6 +12,7 @@ import {
// test suits
import rendering from './rendering'
import client from './client'
+import csp from './csp'
const context = {}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
@@ -30,4 +31,5 @@ describe('Document and App', () => {
rendering(context, 'Rendering via HTTP', (p, q) => renderViaHTTP(context.appPort, p, q), (p, q) => fetchViaHTTP(context.appPort, p, q))
client(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+ csp(context, (p, q) => renderViaHTTP(context.appPort, p, q))
})