-
-
Notifications
You must be signed in to change notification settings - Fork 424
Expand file tree
/
Copy pathsecurity-headers.ts
More file actions
117 lines (106 loc) · 3.78 KB
/
security-headers.ts
File metadata and controls
117 lines (106 loc) · 3.78 KB
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
import { defineNuxtModule, useNuxt } from 'nuxt/kit'
import { BLUESKY_API } from '#shared/utils/constants'
import { ALL_KNOWN_GIT_API_ORIGINS } from '#shared/utils/git-providers'
import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
/**
* Adds Content-Security-Policy and other security headers to all pages.
*
* CSP is delivered via a <meta http-equiv> tag in <head>, so it naturally
* only applies to HTML pages (not API routes). The remaining security
* headers are set via a catch-all route rule.
*
* Note: frame-ancestors is not supported in meta-tag CSP, but
* X-Frame-Options: DENY (set via route rule) provides equivalent protection.
*
* Current policy uses 'unsafe-inline' for scripts and styles because:
* - Nuxt injects inline scripts for hydration and payload transfer
* - Vue uses inline styles for :style bindings and scoped CSS
*/
export default defineNuxtModule({
meta: { name: 'security-headers' },
setup() {
const nuxt = useNuxt()
const devtools = nuxt.options.devtools
const isDevtoolsRuntime =
nuxt.options.dev &&
devtools !== false &&
(devtools == null || typeof devtools !== 'object' || devtools.enabled !== false) &&
!process.env.TEST
// These assets are embedded directly on blog pages and should not affect image-proxy trust.
const cspOnlyImgOrigins = ['https://api.star-history.com', 'https://cdn.bsky.app']
const imgSrc = [
"'self'",
'data:',
...TRUSTED_IMAGE_DOMAINS.map(domain => `https://${domain}`),
...cspOnlyImgOrigins,
].join(' ')
const connectSrc = [
"'self'",
'https://*.algolia.net',
'https://registry.npmjs.org',
'https://api.npmjs.org',
'https://npm.antfu.dev',
BLUESKY_API,
...ALL_KNOWN_GIT_API_ORIGINS,
// Local CLI connector (npmx CLI communicates via localhost)
'http://127.0.0.1:*',
// Devtools runtime (Vue Devtools, Nuxt Devtools, etc) — only in dev mode with devtools enabled
...(isDevtoolsRuntime ? ['ws://localhost:*'] : []),
].join(' ')
const frameSrc = [
'https://bsky.app',
'https://pdsmoover.com',
'https://www.youtube-nocookie.com/',
...(isDevtoolsRuntime ? ["'self'"] : []),
].join(' ')
const securityHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
}
const csp = [
`default-src 'none'`,
`script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`img-src ${imgSrc}`,
`media-src 'self'`,
`font-src 'self'`,
`connect-src ${connectSrc}`,
`frame-src ${frameSrc}`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
`manifest-src 'self'`,
'upgrade-insecure-requests',
].join('; ')
// CSP via <meta> tag — only present in HTML pages, not API responses.
nuxt.options.app.head ??= {}
const head = nuxt.options.app.head as { meta?: Array<Record<string, string>> }
head.meta ??= []
head.meta.push({
'http-equiv': 'Content-Security-Policy',
'content': csp,
})
// Other security headers via route rules (fine on all responses).
nuxt.options.routeRules ??= {}
const wildCardRules = nuxt.options.routeRules['/**']
nuxt.options.routeRules['/**'] = {
...wildCardRules,
headers: {
...wildCardRules?.headers,
...securityHeaders,
},
}
if (!isDevtoolsRuntime) return
const devtoolsRule = nuxt.options.routeRules['/__nuxt_devtools__/**']
nuxt.options.routeRules['/__nuxt_devtools__/**'] = {
...devtoolsRule,
headers: {
...wildCardRules?.headers,
...securityHeaders,
...devtoolsRule?.headers,
'X-Frame-Options': 'SAMEORIGIN',
},
}
},
})