mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +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 consola from 'consola'
|
||||
|
||||
import { isUrl, isPureObject } from '../common/utils'
|
||||
import { isPureObject, isUrl } from '../common/utils'
|
||||
|
||||
import modes from './modes'
|
||||
import defaults from './nuxt.config'
|
||||
@ -130,7 +130,8 @@ Options.from = function (_options) {
|
||||
const cspDefaults = {
|
||||
hashAlgorithm: 'sha256',
|
||||
allowedSources: undefined,
|
||||
policies: undefined
|
||||
policies: undefined,
|
||||
reportOnly: options.debug
|
||||
}
|
||||
if (csp) {
|
||||
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) {
|
||||
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
|
||||
|
@ -3,57 +3,79 @@ import { getPort, loadFixture, Nuxt, rp } from '../utils'
|
||||
let port
|
||||
const url = route => 'http://localhost:' + port + route
|
||||
|
||||
const startCSPTestServer = async (csp) => {
|
||||
const options = loadFixture('basic', { render: { csp } })
|
||||
const startCspServer = async (csp, isProduction = true) => {
|
||||
const options = loadFixture('basic', {
|
||||
debug: !isProduction,
|
||||
render: { csp }
|
||||
})
|
||||
const nuxt = new Nuxt(options)
|
||||
port = await getPort()
|
||||
await nuxt.listen(port, '0.0.0.0')
|
||||
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', () => {
|
||||
let nuxt
|
||||
|
||||
afterEach(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
|
||||
describe('production mode', () => {
|
||||
test(
|
||||
'Not contain Content-Security-Policy header, when csp is false',
|
||||
async () => {
|
||||
const nuxt = await startCSPTestServer(false)
|
||||
nuxt = await startCspServer(false)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
|
||||
expect(headers['content-security-policy']).toBe(undefined)
|
||||
|
||||
await nuxt.close()
|
||||
expect(headers[cspHeader]).toBe(undefined)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Contain Content-Security-Policy header, when csp is set',
|
||||
async () => {
|
||||
const nuxt = await startCSPTestServer(true)
|
||||
nuxt = await startCspServer(true)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
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(
|
||||
'Contain only unique hashes in header when csp is set',
|
||||
async () => {
|
||||
const nuxt = await startCSPTestServer(true)
|
||||
nuxt = await startCspServer(true)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
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)]
|
||||
|
||||
expect(uniqueHashes.length).toBe(hashes.length)
|
||||
|
||||
await nuxt.close()
|
||||
}
|
||||
)
|
||||
|
||||
@ -64,16 +86,14 @@ describe('basic ssr csp', () => {
|
||||
allowedSources: ['https://example.com', 'https://example.io']
|
||||
}
|
||||
|
||||
const nuxt = await startCSPTestServer(cspOption)
|
||||
nuxt = await startCspServer(cspOption)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
|
||||
expect(headers['content-security-policy']).toMatch(/^script-src 'self' 'sha256-.*'/)
|
||||
expect(headers['content-security-policy'].includes('https://example.com')).toBe(true)
|
||||
expect(headers['content-security-policy'].includes('https://example.io')).toBe(true)
|
||||
|
||||
await nuxt.close()
|
||||
expect(headers[cspHeader]).toMatch(/^script-src 'self' 'sha256-.*'/)
|
||||
expect(headers[cspHeader].includes('https://example.com')).toBe(true)
|
||||
expect(headers[cspHeader].includes('https://example.io')).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
@ -88,17 +108,15 @@ describe('basic ssr csp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const nuxt = await startCSPTestServer(cspOption)
|
||||
nuxt = await startCspServer(cspOption)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
|
||||
expect(headers['content-security-policy']).toMatch(/default-src 'none'/)
|
||||
expect(headers['content-security-policy']).toMatch(/script-src 'sha256-(.*)?' 'self'/)
|
||||
expect(headers['content-security-policy'].includes('https://example.com')).toBe(true)
|
||||
expect(headers['content-security-policy'].includes('https://example.io')).toBe(true)
|
||||
|
||||
await nuxt.close()
|
||||
expect(headers[cspHeader]).toMatch(/default-src 'none'/)
|
||||
expect(headers[cspHeader]).toMatch(/script-src 'sha256-(.*)?' 'self'/)
|
||||
expect(headers[cspHeader].includes('https://example.com')).toBe(true)
|
||||
expect(headers[cspHeader].includes('https://example.io')).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
@ -112,15 +130,13 @@ describe('basic ssr csp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const nuxt = await startCSPTestServer(cspOption)
|
||||
nuxt = await startCspServer(cspOption)
|
||||
const { headers } = await rp(url('/stateless'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
|
||||
expect(headers['content-security-policy']).toMatch(/default-src 'none'/)
|
||||
expect(headers['content-security-policy']).toMatch(/script-src 'sha256-.*' 'self'$/)
|
||||
|
||||
await nuxt.close()
|
||||
expect(headers[cspHeader]).toMatch(/default-src 'none'/)
|
||||
expect(headers[cspHeader]).toMatch(/script-src 'sha256-.*' 'self'$/)
|
||||
}
|
||||
)
|
||||
|
||||
@ -133,7 +149,7 @@ describe('basic ssr csp', () => {
|
||||
'style-src': [`'self'`]
|
||||
}
|
||||
|
||||
const nuxt = await startCSPTestServer({
|
||||
nuxt = await startCspServer({
|
||||
policies
|
||||
})
|
||||
|
||||
@ -147,12 +163,154 @@ describe('basic ssr csp', () => {
|
||||
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)]
|
||||
|
||||
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