mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-30 09:27:13 +00:00
feat(csp): add reportOnly option (#3559)
This commit is contained in:
parent
e5a4149a66
commit
8c85f2299e
@ -4,7 +4,7 @@ import fs from 'fs'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import consola from 'consola'
|
import consola from 'consola'
|
||||||
|
|
||||||
import { isUrl, isPureObject } from '../common/utils'
|
import { isPureObject, isUrl } from '../common/utils'
|
||||||
|
|
||||||
import modes from './modes'
|
import modes from './modes'
|
||||||
import defaults from './nuxt.config'
|
import defaults from './nuxt.config'
|
||||||
@ -130,7 +130,8 @@ Options.from = function (_options) {
|
|||||||
const cspDefaults = {
|
const cspDefaults = {
|
||||||
hashAlgorithm: 'sha256',
|
hashAlgorithm: 'sha256',
|
||||||
allowedSources: undefined,
|
allowedSources: undefined,
|
||||||
policies: undefined
|
policies: undefined,
|
||||||
|
reportOnly: options.debug
|
||||||
}
|
}
|
||||||
if (csp) {
|
if (csp) {
|
||||||
options.render.csp = _.defaults(_.isObject(csp) ? csp : {}, cspDefaults)
|
options.render.csp = _.defaults(_.isObject(csp) ? csp : {}, cspDefaults)
|
||||||
|
@ -70,7 +70,9 @@ export default async function nuxtMiddleware(req, res, next) {
|
|||||||
|
|
||||||
if (this.options.render.csp) {
|
if (this.options.render.csp) {
|
||||||
const { allowedSources, policies } = this.options.render.csp
|
const { allowedSources, policies } = this.options.render.csp
|
||||||
res.setHeader('Content-Security-Policy', getCspString({ cspScriptSrcHashSet, allowedSources, policies, isDev: this.options.dev }))
|
const cspHeader = this.options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
|
||||||
|
|
||||||
|
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashSet, allowedSources, policies, isDev: this.options.dev }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
|
@ -3,57 +3,79 @@ import { getPort, loadFixture, Nuxt, rp } from '../utils'
|
|||||||
let port
|
let port
|
||||||
const url = route => 'http://localhost:' + port + route
|
const url = route => 'http://localhost:' + port + route
|
||||||
|
|
||||||
const startCSPTestServer = async (csp) => {
|
const startCspServer = async (csp, isProduction = true) => {
|
||||||
const options = loadFixture('basic', { render: { csp } })
|
const options = loadFixture('basic', {
|
||||||
|
debug: !isProduction,
|
||||||
|
render: { csp }
|
||||||
|
})
|
||||||
const nuxt = new Nuxt(options)
|
const nuxt = new Nuxt(options)
|
||||||
port = await getPort()
|
port = await getPort()
|
||||||
await nuxt.listen(port, '0.0.0.0')
|
await nuxt.listen(port, '0.0.0.0')
|
||||||
return nuxt
|
return nuxt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getHeader = debug => debug ? 'content-security-policy-report-only' : 'content-security-policy'
|
||||||
|
const cspHeader = getHeader(false)
|
||||||
|
const reportOnlyHeader = getHeader(true)
|
||||||
|
|
||||||
|
const startCspDevServer = async csp => startCspServer(csp, false)
|
||||||
|
|
||||||
describe('basic ssr csp', () => {
|
describe('basic ssr csp', () => {
|
||||||
|
let nuxt
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await nuxt.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('production mode', () => {
|
||||||
test(
|
test(
|
||||||
'Not contain Content-Security-Policy header, when csp is false',
|
'Not contain Content-Security-Policy header, when csp is false',
|
||||||
async () => {
|
async () => {
|
||||||
const nuxt = await startCSPTestServer(false)
|
nuxt = await startCspServer(false)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers['content-security-policy']).toBe(undefined)
|
expect(headers[cspHeader]).toBe(undefined)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Contain Content-Security-Policy header, when csp is set',
|
'Contain Content-Security-Policy header, when csp is set',
|
||||||
async () => {
|
async () => {
|
||||||
const nuxt = await startCSPTestServer(true)
|
nuxt = await startCspServer(true)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers['content-security-policy']).toMatch(/^script-src 'self' 'sha256-.*'$/)
|
expect(headers[cspHeader]).toMatch(/^script-src 'self' 'sha256-.*'$/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
await nuxt.close()
|
test(
|
||||||
|
'Contain Content-Security-Policy-Report-Only header, when explicitly asked for',
|
||||||
|
async () => {
|
||||||
|
nuxt = await startCspDevServer({reportOnly: true})
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/^script-src 'self' 'sha256-.*'$/)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Contain only unique hashes in header when csp is set',
|
'Contain only unique hashes in header when csp is set',
|
||||||
async () => {
|
async () => {
|
||||||
const nuxt = await startCSPTestServer(true)
|
nuxt = await startCspServer(true)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const hashes = headers['content-security-policy'].split(' ').filter(s => s.startsWith('\'sha256-'))
|
const hashes = headers[cspHeader].split(' ').filter(s => s.startsWith('\'sha256-'))
|
||||||
const uniqueHashes = [...new Set(hashes)]
|
const uniqueHashes = [...new Set(hashes)]
|
||||||
|
|
||||||
expect(uniqueHashes.length).toBe(hashes.length)
|
expect(uniqueHashes.length).toBe(hashes.length)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,16 +86,14 @@ describe('basic ssr csp', () => {
|
|||||||
allowedSources: ['https://example.com', 'https://example.io']
|
allowedSources: ['https://example.com', 'https://example.io']
|
||||||
}
|
}
|
||||||
|
|
||||||
const nuxt = await startCSPTestServer(cspOption)
|
nuxt = await startCspServer(cspOption)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers['content-security-policy']).toMatch(/^script-src 'self' 'sha256-.*'/)
|
expect(headers[cspHeader]).toMatch(/^script-src 'self' 'sha256-.*'/)
|
||||||
expect(headers['content-security-policy'].includes('https://example.com')).toBe(true)
|
expect(headers[cspHeader].includes('https://example.com')).toBe(true)
|
||||||
expect(headers['content-security-policy'].includes('https://example.io')).toBe(true)
|
expect(headers[cspHeader].includes('https://example.io')).toBe(true)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -88,17 +108,15 @@ describe('basic ssr csp', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nuxt = await startCSPTestServer(cspOption)
|
nuxt = await startCspServer(cspOption)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers['content-security-policy']).toMatch(/default-src 'none'/)
|
expect(headers[cspHeader]).toMatch(/default-src 'none'/)
|
||||||
expect(headers['content-security-policy']).toMatch(/script-src 'sha256-(.*)?' 'self'/)
|
expect(headers[cspHeader]).toMatch(/script-src 'sha256-(.*)?' 'self'/)
|
||||||
expect(headers['content-security-policy'].includes('https://example.com')).toBe(true)
|
expect(headers[cspHeader].includes('https://example.com')).toBe(true)
|
||||||
expect(headers['content-security-policy'].includes('https://example.io')).toBe(true)
|
expect(headers[cspHeader].includes('https://example.io')).toBe(true)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -112,15 +130,13 @@ describe('basic ssr csp', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nuxt = await startCSPTestServer(cspOption)
|
nuxt = await startCspServer(cspOption)
|
||||||
const { headers } = await rp(url('/stateless'), {
|
const { headers } = await rp(url('/stateless'), {
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers['content-security-policy']).toMatch(/default-src 'none'/)
|
expect(headers[cspHeader]).toMatch(/default-src 'none'/)
|
||||||
expect(headers['content-security-policy']).toMatch(/script-src 'sha256-.*' 'self'$/)
|
expect(headers[cspHeader]).toMatch(/script-src 'sha256-.*' 'self'$/)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -133,7 +149,7 @@ describe('basic ssr csp', () => {
|
|||||||
'style-src': [`'self'`]
|
'style-src': [`'self'`]
|
||||||
}
|
}
|
||||||
|
|
||||||
const nuxt = await startCSPTestServer({
|
nuxt = await startCspServer({
|
||||||
policies
|
policies
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -147,12 +163,154 @@ describe('basic ssr csp', () => {
|
|||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const hashes = headers['content-security-policy'].split(' ').filter(s => s.startsWith('\'sha256-'))
|
const hashes = headers[cspHeader].split(' ').filter(s => s.startsWith('\'sha256-'))
|
||||||
const uniqueHashes = [...new Set(hashes)]
|
const uniqueHashes = [...new Set(hashes)]
|
||||||
|
|
||||||
expect(uniqueHashes.length).toBe(hashes.length)
|
expect(uniqueHashes.length).toBe(hashes.length)
|
||||||
|
|
||||||
await nuxt.close()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
describe('debug mode', () => {
|
||||||
|
test(
|
||||||
|
'Not contain Content-Security-Policy-Report-Only header, when csp is false',
|
||||||
|
async () => {
|
||||||
|
nuxt = await startCspDevServer(false)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toBe(undefined)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain Content-Security-Policy header, when explicitly asked for',
|
||||||
|
async () => {
|
||||||
|
nuxt = await startCspDevServer({reportOnly: false})
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[cspHeader]).toMatch(/^script-src 'self' 'sha256-.*'$/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain Content-Security-Policy header, when csp is set',
|
||||||
|
async () => {
|
||||||
|
nuxt = await startCspDevServer(true)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/^script-src 'self' 'sha256-.*'$/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain only unique hashes in header when csp is set',
|
||||||
|
async () => {
|
||||||
|
nuxt = await startCspDevServer(true)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const hashes = headers[reportOnlyHeader].split(' ').filter(s => s.startsWith('\'sha256-'))
|
||||||
|
const uniqueHashes = [...new Set(hashes)]
|
||||||
|
|
||||||
|
expect(uniqueHashes.length).toBe(hashes.length)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain Content-Security-Policy-Report-Only header, when csp.allowedSources set',
|
||||||
|
async () => {
|
||||||
|
const cspOption = {
|
||||||
|
allowedSources: ['https://example.com', 'https://example.io']
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxt = await startCspDevServer(cspOption)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/^script-src 'self' 'sha256-.*'/)
|
||||||
|
expect(headers[reportOnlyHeader].includes('https://example.com')).toBe(true)
|
||||||
|
expect(headers[reportOnlyHeader].includes('https://example.io')).toBe(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain Content-Security-Policy-Report-Only header, when csp.policies set',
|
||||||
|
async () => {
|
||||||
|
const cspOption = {
|
||||||
|
enabled: true,
|
||||||
|
policies: {
|
||||||
|
'default-src': [`'none'`],
|
||||||
|
'script-src': ['https://example.com', 'https://example.io']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxt = await startCspDevServer(cspOption)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/default-src 'none'/)
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/script-src 'sha256-(.*)?' 'self'/)
|
||||||
|
expect(headers[reportOnlyHeader].includes('https://example.com')).toBe(true)
|
||||||
|
expect(headers[reportOnlyHeader].includes('https://example.io')).toBe(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain Content-Security-Policy-Report-Only header, when csp.policies.script-src is not set',
|
||||||
|
async () => {
|
||||||
|
const cspOption = {
|
||||||
|
enabled: true,
|
||||||
|
policies: {
|
||||||
|
'default-src': [`'none'`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxt = await startCspDevServer(cspOption)
|
||||||
|
const { headers } = await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/default-src 'none'/)
|
||||||
|
expect(headers[reportOnlyHeader]).toMatch(/script-src 'sha256-.*' 'self'$/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Contain only unique hashes in header when csp.policies is set',
|
||||||
|
async () => {
|
||||||
|
const policies = {
|
||||||
|
'default-src': [`'self'`],
|
||||||
|
'script-src': [`'self'`],
|
||||||
|
'style-src': [`'self'`]
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxt = await startCspDevServer({
|
||||||
|
policies
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await rp(url('/stateless'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headers } = await rp(url('/stateful'), {
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const hashes = headers[reportOnlyHeader].split(' ').filter(s => s.startsWith('\'sha256-'))
|
||||||
|
const uniqueHashes = [...new Set(hashes)]
|
||||||
|
|
||||||
|
expect(uniqueHashes.length).toBe(hashes.length)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user