-
-
Notifications
You must be signed in to change notification settings - Fork 425
Expand file tree
/
Copy pathsecurity-headers.ts
More file actions
83 lines (77 loc) · 2.82 KB
/
security-headers.ts
File metadata and controls
83 lines (77 loc) · 2.82 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
import { defineNuxtModule } 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(_, nuxt) {
// 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:*',
].join(' ')
const frameSrc = ['https://bsky.app', 'https://pdsmoover.com'].join(' ')
const csp = [
`default-src 'none'`,
`script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`img-src ${imgSrc}`,
`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,
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
},
}
},
})