-
Notifications
You must be signed in to change notification settings - Fork 65
/
Copy path02-cspSsg.ts
123 lines (102 loc) · 3.35 KB
/
02-cspSsg.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import path from 'node:path'
import crypto from 'node:crypto'
import type { NitroAppPlugin } from 'nitropack'
import type { H3Event } from 'h3'
import defu from 'defu'
import type {
ModuleOptions
} from '../../../types'
import type {
ContentSecurityPolicyValue
} from '../../../types/headers'
import { useRuntimeConfig } from '#imports'
interface NuxtRenderHTMLContext {
island?: boolean
htmlAttrs: string[]
head: string[]
bodyAttrs: string[]
bodyPrepend: string[]
body: string[]
bodyAppend: string[]
}
const moduleOptions = useRuntimeConfig().security as ModuleOptions
export default <NitroAppPlugin> function (nitro) {
nitro.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => {
// Content Security Policy
if (!isContentSecurityPolicyEnabled(event, moduleOptions)) {
return
}
if (!moduleOptions.headers) {
return
}
const scriptPattern = /<script[^>]*>(.*?)<\/script>/gs
const scriptHashes: string[] = []
const hashAlgorithm = 'sha256'
let match
while ((match = scriptPattern.exec(html.bodyAppend.join(''))) !== null) {
if (match[1]) {
scriptHashes.push(generateHash(match[1], hashAlgorithm))
}
}
const cspConfig = moduleOptions.headers.contentSecurityPolicy
if (cspConfig && typeof cspConfig !== 'string') {
html.head.push(generateCspMetaTag(cspConfig, scriptHashes))
}
})
function generateCspMetaTag (policies: ContentSecurityPolicyValue, scriptHashes: string[]) {
const unsupportedPolicies = {
'frame-ancestors': true,
'report-uri': true,
sandbox: true
}
const tagPolicies = defu(policies) as ContentSecurityPolicyValue
if (scriptHashes.length > 0 && moduleOptions.ssg?.hashScripts) {
// Remove '""'
tagPolicies['script-src'] = (tagPolicies['script-src'] ?? []).concat(scriptHashes)
}
const contentArray: string[] = []
for (const [key, value] of Object.entries(tagPolicies)) {
if (unsupportedPolicies[key]) {
continue
}
let policyValue: string
if (Array.isArray(value)) {
policyValue = value.join(' ')
} else if (typeof value === 'boolean') {
policyValue = ''
} else {
policyValue = value
}
if (value !== false) {
contentArray.push(`${key} ${policyValue}`)
}
}
const content = contentArray.join('; ')
return `<meta http-equiv="Content-Security-Policy" content="${content}">`
}
function generateHash (content: string, hashAlgorithm: string) {
const hash = crypto.createHash(hashAlgorithm)
hash.update(content)
return `'${hashAlgorithm}-${hash.digest('base64')}'`
}
/**
* Only enable behavior if Content Security pPolicy is enabled,
* initial page is prerendered and generated file type is HTML.
* @param event H3Event
* @param options ModuleOptions
* @returns boolean
*/
function isContentSecurityPolicyEnabled (event: H3Event, options: ModuleOptions): boolean {
const nitroPrerenderHeader = 'x-nitro-prerender'
const nitroPrerenderHeaderValue = event.node.req.headers[nitroPrerenderHeader]
// Page is not prerendered
if (!nitroPrerenderHeaderValue) {
return false
}
// File is not HTML
if (!['', '.html'].includes(path.extname(nitroPrerenderHeaderValue as string))) {
return false
}
return true
}
}