Skip to content

Commit

Permalink
feat: extend CSP support in SSG mode
Browse files Browse the repository at this point in the history
  • Loading branch information
vejja committed Oct 30, 2023
1 parent e8b3651 commit 16f169d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 11 deletions.
1 change: 1 addition & 0 deletions .stackblitz/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
.output
.env
dist
.vercel
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const removeCspHeaderForPrerenderedRoutes = (nuxt: Nuxt) => {
const nitroRouteRules = nuxt.options.nitro.routeRules
for (const route in nitroRouteRules) {
const routeRules = nitroRouteRules[route]
if (routeRules.prerender) {
if (routeRules.prerender || nuxt.options.nitro.static) {
routeRules.headers = routeRules.headers || {}
routeRules.headers['Content-Security-Policy'] = ''
}
Expand Down
70 changes: 60 additions & 10 deletions src/runtime/nitro/plugins/02-cspSsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type {
import type {
ContentSecurityPolicyValue
} from '../../../types/headers'
import { defineNitroPlugin, useRuntimeConfig } from '#imports'
import { defineNitroPlugin, useRuntimeConfig, getRouteRules } from '#imports'
import { useNitro } from '@nuxt/kit'

const moduleOptions = useRuntimeConfig().security

Expand All @@ -24,25 +25,54 @@ export default defineNitroPlugin((nitroApp) => {
return
}

const scriptPattern = /<script[^>]*>(.*?)<\/script>/gs
// Detect bothe inline scripts and inline styles
const inlineScriptPattern = /<script[^>]*>(.*?)<\/script>/gs
const inlineStylePattern = /<style>(.*?)<\/style>/gs
// Whitelist external scripts based on integrity attribute
const externalScriptPattern = /<script .*?integrity="(.*?)".*?(\/>|>.*?<\/script>)/gs
const scriptHashes: string[] = []
const styleHashes: string[] = []
const hashAlgorithm = 'sha256'

let match
while ((match = scriptPattern.exec(html.bodyAppend.join(''))) !== null) {
if (match[1]) {
scriptHashes.push(generateHash(match[1], hashAlgorithm))
// Scan all relevant sections of the NuxtRenderHtmlContext
for (const section of ['body', 'bodyAppend', 'bodyPrepend', 'head']) {
const htmlRecords = html as unknown as Record<string, string[]>
const elements = htmlRecords[section]
for (const element of elements) {
let match
while ((match = inlineScriptPattern.exec(element)) !== null) {
if (match[1]) {
scriptHashes.push(generateHash(match[1], hashAlgorithm))
}
}
while ((match = inlineStylePattern.exec(element)) !== null) {
if (match[1]) {
styleHashes.push(generateHash(match[1], hashAlgorithm))
}
}
while ((match = externalScriptPattern.exec(element)) !== null) {
if (match[1]) {
scriptHashes.push(`'${match[1]}'`)
}
}
}
}

const cspConfig = moduleOptions.headers.contentSecurityPolicy

if (cspConfig && typeof cspConfig !== 'string') {
html.head.push(generateCspMetaTag(cspConfig, scriptHashes))
const content = generateCspMetaTag(cspConfig, scriptHashes, styleHashes)
// Insert hashes in the http meta tag
html.head.push(`<meta http-equiv="Content-Security-Policy" content="${content}">`)
// Also insert hashes in static headers for presets that generate headers rules for static files
updateRouteRules(event, content)
}


})

function generateCspMetaTag (policies: ContentSecurityPolicyValue, scriptHashes: string[]) {
// Insert hashes in the CSP meta tag for both the script-src and the style-src policies
function generateCspMetaTag (policies: ContentSecurityPolicyValue, scriptHashes: string[], styleHashes: string[]) {
const unsupportedPolicies:Record<string, boolean> = {
'frame-ancestors': true,
'report-uri': true,
Expand All @@ -54,6 +84,10 @@ export default defineNitroPlugin((nitroApp) => {
// Remove '""'
tagPolicies['script-src'] = (tagPolicies['script-src'] ?? []).concat(scriptHashes)
}
if (styleHashes.length > 0 && moduleOptions.ssg?.hashScripts) {
// Remove '""'
tagPolicies['style-src'] = (tagPolicies['style-src'] ?? []).concat(styleHashes)
}

const contentArray: string[] = []
for (const [key, value] of Object.entries(tagPolicies)) {
Expand All @@ -75,9 +109,25 @@ export default defineNitroPlugin((nitroApp) => {
contentArray.push(`${key} ${policyValue}`)
}
}
const content = contentArray.join('; ')
const content = contentArray.join('; ').replaceAll("'nonce-{{nonce}}'", '')
return content
}

return `<meta http-equiv="Content-Security-Policy" content="${content}">`
// In some Nitro presets (e.g. Vercel), the header rules are generated for the static server
// By default we update the nitro route rules with their calculated CSP value to support this possibility
function updateRouteRules(event: H3Event, content: string) {
const path = event.path
const routeRules = getRouteRules(event)
let headers
if (routeRules.headers) {
headers = { ...routeRules.headers }
} else {
headers = {}
}
headers['Content-Security-Policy'] = content
routeRules.headers = headers
const nitro = useNitro()
nitro.options.routeRules[path] = routeRules
}

function generateHash (content: string, hashAlgorithm: string) {
Expand Down

0 comments on commit 16f169d

Please sign in to comment.