Skip to content

Commit a22dff5

Browse files
committed
fix: also apply security headers to prerendered pages
1 parent 5ff3953 commit a22dff5

File tree

3 files changed

+73
-82
lines changed

3 files changed

+73
-82
lines changed

modules/security-headers.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { defineNuxtModule } from 'nuxt/kit'
2+
import { ALL_KNOWN_GIT_API_ORIGINS } from '#shared/utils/git-providers'
3+
import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
4+
5+
/**
6+
* Adds Content-Security-Policy and security headers to all HTML responses
7+
* via a Nitro route rule. This covers both SSR/ISR pages and prerendered
8+
* pages (which don't run server middleware).
9+
*
10+
* Current policy uses 'unsafe-inline' for scripts and styles because:
11+
* - Nuxt injects inline scripts for hydration and payload transfer
12+
* - Vue uses inline styles for :style bindings and scoped CSS
13+
*/
14+
export default defineNuxtModule({
15+
meta: { name: 'security-headers' },
16+
setup(_, nuxt) {
17+
const imgSrc = [
18+
"'self'",
19+
'data:',
20+
...TRUSTED_IMAGE_DOMAINS.map(domain => `https://${domain}`),
21+
].join(' ')
22+
23+
const connectSrc = [
24+
"'self'",
25+
'https://*.algolia.net',
26+
'https://registry.npmjs.org',
27+
'https://api.npmjs.org',
28+
'https://npm.antfu.dev',
29+
...ALL_KNOWN_GIT_API_ORIGINS,
30+
].join(' ')
31+
32+
const frameSrc = ['https://bsky.app', 'https://pdsmoover.com'].join(' ')
33+
34+
const csp = [
35+
`default-src 'none'`,
36+
`script-src 'self' 'unsafe-inline'`,
37+
`style-src 'self' 'unsafe-inline'`,
38+
`img-src ${imgSrc}`,
39+
`font-src 'self'`,
40+
`connect-src ${connectSrc}`,
41+
`frame-src ${frameSrc}`,
42+
`frame-ancestors 'none'`,
43+
`base-uri 'self'`,
44+
`form-action 'self'`,
45+
`object-src 'none'`,
46+
`manifest-src 'self'`,
47+
'upgrade-insecure-requests',
48+
].join('; ')
49+
50+
const headers = {
51+
'Content-Security-Policy': csp,
52+
'X-Content-Type-Options': 'nosniff',
53+
'X-Frame-Options': 'DENY',
54+
'Referrer-Policy': 'strict-origin-when-cross-origin',
55+
}
56+
57+
// Apply to all page routes via a catch-all rule.
58+
// API routes are excluded — CSP doesn't make sense for JSON responses.
59+
nuxt.options.routeRules ??= {}
60+
nuxt.options.routeRules['/**'] = {
61+
...nuxt.options.routeRules['/**'],
62+
headers,
63+
}
64+
nuxt.options.routeRules['/api/**'] = {
65+
...nuxt.options.routeRules['/api/**'],
66+
headers: {
67+
'X-Content-Type-Options': 'nosniff',
68+
},
69+
}
70+
},
71+
})

server/middleware/security-headers.global.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

test/e2e/security-headers.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ test.describe('security headers', () => {
1212
expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin')
1313
})
1414

15-
test('API routes do not include CSP headers', async ({ page, baseURL }) => {
15+
test('API routes do not include CSP', async ({ page, baseURL }) => {
1616
const response = await page.request.get(`${baseURL}/api/registry/package-meta/vue`)
17-
const headers = response.headers()
1817

19-
expect(headers['content-security-policy']).toBeFalsy()
18+
expect(response.headers()['content-security-policy']).toBeUndefined()
2019
})
2120

2221
// Navigate key pages and assert no CSP violations are logged.

0 commit comments

Comments
 (0)