Skip to content

Commit

Permalink
Merge pull request #287 from vejja/csp-ssg-sri
Browse files Browse the repository at this point in the history
feat(csp): SRI hashes for SSG mode
  • Loading branch information
Baroshem authored Nov 10, 2023
2 parents 64a2aa4 + 6c5ec9e commit f75159d
Show file tree
Hide file tree
Showing 16 changed files with 395 additions and 54 deletions.
55 changes: 45 additions & 10 deletions src/runtime/nitro/plugins/02-cspSsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export default defineNitroPlugin((nitroApp) => {
return
}

const scriptHashes: string[] = []
const styleHashes: string[] = []
const scriptHashes: Set<string> = new Set()
const styleHashes: Set<string> = new Set()
const hashAlgorithm = 'sha256'

// Scan all relevant sections of the NuxtRenderHtmlContext
Expand All @@ -42,10 +42,10 @@ export default defineNitroPlugin((nitroApp) => {
const integrity = scriptAttrs?.integrity
if (!src && scriptText) {
// Hash inline scripts with content
scriptHashes.push(generateHash(scriptText, hashAlgorithm))
scriptHashes.add(generateHash(scriptText, hashAlgorithm))
} else if (src && integrity) {
// Whitelist external scripts with integrity
scriptHashes.push(`'${integrity}'`)
scriptHashes.add(`'${integrity}'`)
}
})

Expand All @@ -54,7 +54,41 @@ export default defineNitroPlugin((nitroApp) => {
const styleText = $(style).text()
if (styleText) {
// Hash inline styles with content
styleHashes.push(generateHash(styleText, hashAlgorithm))
styleHashes.add(generateHash(styleText, hashAlgorithm))
}
})

// Parse all link tags
$('link').each((i, link) => {
const linkAttrs = $(link).attr()
const integrity = linkAttrs?.integrity
// Whitelist links to external resources with integrity
if (integrity) {
const rel = linkAttrs?.rel
// HTML standard defines only 3 rel values for valid integrity attributes on links : stylesheet, preload and modulepreload
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-integrity
if (rel === 'stylesheet') {
// style: add to style-src
styleHashes.add(`'${integrity}'`)
} else if (rel === 'preload') {
// Fetch standard defines the destination (https://fetch.spec.whatwg.org/#destination-table)
// This table is the official mapping between HTML and CSP
// We only support script-src for now, but we could populate other policies in the future
const as = linkAttrs.as
switch (as) {
case 'script':
case 'audioworklet':
case 'paintworklet':
case 'xlst':
scriptHashes.add(`'${integrity}'`)
break
default:
break
}
} else if (rel === 'modulepreload') {
// script is the default and only possible destination
scriptHashes.add(`'${integrity}'`)
}
}
})
}
Expand All @@ -73,21 +107,22 @@ export default defineNitroPlugin((nitroApp) => {
})

// 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[]) {
function generateCspMetaTag (policies: ContentSecurityPolicyValue, scriptHashes: Set<string>, styleHashes: Set<string>) {
const unsupportedPolicies:Record<string, boolean> = {
'frame-ancestors': true,
'report-uri': true,
sandbox: true
}

const tagPolicies = defu(policies) as ContentSecurityPolicyValue
if (scriptHashes.length > 0 && moduleOptions.ssg?.hashScripts) {
if (scriptHashes.size > 0 && moduleOptions.ssg?.hashScripts) {
// Remove '""'
tagPolicies['script-src'] = (tagPolicies['script-src'] ?? []).concat(scriptHashes)
tagPolicies['script-src'] = (tagPolicies['script-src'] ?? []).concat(...scriptHashes)
}
if (styleHashes.length > 0 && moduleOptions.ssg?.hashStyles) {

if (styleHashes.size > 0 && moduleOptions.ssg?.hashStyles) {
// Remove '""'
tagPolicies['style-src'] = (tagPolicies['style-src'] ?? []).concat(styleHashes)
tagPolicies['style-src'] = (tagPolicies['style-src'] ?? []).concat(...styleHashes)
}

const contentArray: string[] = []
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/ssg/.nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
imports.autoImport=true
42 changes: 42 additions & 0 deletions test/fixtures/ssg/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({

modules: ['../../../src/module'],

// Per route configuration
routeRules: {
'/': {
prerender: true
},
'/inline-script': {
prerender: true,
},
'/inline-style': {
prerender: true
},
'/external-script': {
prerender: true
},
'/external-style': {
prerender: true
},
'/external-link': {
prerender: true
},
'/not-ssg': {
prerender: false
}
},

// Global configuration
security: {
rateLimiter: false,
sri: true,
ssg: {
hashScripts: true,
hashStyles: true
}
},

})
5 changes: 5 additions & 0 deletions test/fixtures/ssg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"private": true,
"name": "basic",
"type": "module"
}
12 changes: 12 additions & 0 deletions test/fixtures/ssg/pages/external-link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
link: [
{ rel: 'icon', href: '/icon.png' }
]
})
</script>
12 changes: 12 additions & 0 deletions test/fixtures/ssg/pages/external-script.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
script: [
{ src: '/external.js' }
]
})
</script>
12 changes: 12 additions & 0 deletions test/fixtures/ssg/pages/external-style.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
link: [
{ rel: 'stylesheet', href: '/external.css' }
]
})
</script>
5 changes: 5 additions & 0 deletions test/fixtures/ssg/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
Default page
</div>
</template>
12 changes: 12 additions & 0 deletions test/fixtures/ssg/pages/inline-script.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
script: [
{ textContent: 'window.myImportantVar = 1', type: 'text/javascript' }
]
})
</script>
12 changes: 12 additions & 0 deletions test/fixtures/ssg/pages/inline-style.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
style: [
{ textContent: 'div { color: blue }', type: 'text/css' }
]
})
</script>
17 changes: 17 additions & 0 deletions test/fixtures/ssg/pages/not-ssg.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div>
Default page
</div>
</template>
<script setup>
useHead({
script: [
{ src: '/external.js' },
{ textContent: 'window.myImportantVar = 4', type: 'text/javascript' }
],
link: [
{ rel: 'stylesheet', href: '/external.css' },
{ rel: 'icon', href: '/icon.png' }
]
})
</script>
3 changes: 3 additions & 0 deletions test/fixtures/ssg/public/external.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
div {
color: red;
}
1 change: 1 addition & 0 deletions test/fixtures/ssg/public/external.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('Hello World')
Binary file added test/fixtures/ssg/public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit f75159d

Please sign in to comment.