Skip to content

Commit c853088

Browse files
committed
fix: relax devtools-only CSP and frame headers in dev
1 parent e5f70f1 commit c853088

2 files changed

Lines changed: 152 additions & 11 deletions

File tree

modules/security-headers.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineNuxtModule } from 'nuxt/kit'
1+
import { defineNuxtModule, useNuxt } from 'nuxt/kit'
22
import { BLUESKY_API } from '#shared/utils/constants'
33
import { ALL_KNOWN_GIT_API_ORIGINS } from '#shared/utils/git-providers'
44
import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
@@ -19,13 +19,15 @@ import { TRUSTED_IMAGE_DOMAINS } from '#server/utils/image-proxy'
1919
*/
2020
export default defineNuxtModule({
2121
meta: { name: 'security-headers' },
22-
setup(_, nuxt) {
23-
const isDevtoolsRuntime =
24-
nuxt.options.dev && nuxt.options.devtools !== false && !process.env.TEST
22+
setup() {
23+
const nuxt = useNuxt()
24+
const devtools = nuxt.options.devtools
2525

26-
// Nuxt DevTools relies on injected client assets and an iframe-based UI in dev.
27-
// Keep strict CSP/frame restrictions for non-dev environments.
28-
if (isDevtoolsRuntime) return
26+
const isDevtoolsRuntime =
27+
nuxt.options.dev
28+
&& devtools !== false
29+
&& (devtools == null || typeof devtools !== 'object' || devtools.enabled !== false)
30+
&& !process.env.TEST
2931

3032
// These assets are embedded directly on blog pages and should not affect image-proxy trust.
3133
const cspOnlyImgOrigins = ['https://api.star-history.com', 'https://cdn.bsky.app']
@@ -46,9 +48,21 @@ export default defineNuxtModule({
4648
...ALL_KNOWN_GIT_API_ORIGINS,
4749
// Local CLI connector (npmx CLI communicates via localhost)
4850
'http://127.0.0.1:*',
51+
// Devtools runtime (Vue Devtools, Nuxt Devtools, etc) — only in dev mode with devtools enabled
52+
...(isDevtoolsRuntime ? ['ws://localhost:*'] : []),
53+
].join(' ')
54+
55+
const frameSrc = [
56+
'https://bsky.app',
57+
'https://pdsmoover.com',
58+
...(isDevtoolsRuntime ? ["'self'"] : []),
4959
].join(' ')
5060

51-
const frameSrc = ['https://bsky.app', 'https://pdsmoover.com'].join(' ')
61+
const securityHeaders = {
62+
'X-Content-Type-Options': 'nosniff',
63+
'X-Frame-Options': 'DENY',
64+
'Referrer-Policy': 'strict-origin-when-cross-origin',
65+
}
5266

5367
const csp = [
5468
`default-src 'none'`,
@@ -81,9 +95,21 @@ export default defineNuxtModule({
8195
...wildCardRules,
8296
headers: {
8397
...wildCardRules?.headers,
84-
'X-Content-Type-Options': 'nosniff',
85-
'X-Frame-Options': 'DENY',
86-
'Referrer-Policy': 'strict-origin-when-cross-origin',
98+
...securityHeaders,
99+
},
100+
}
101+
102+
if (!isDevtoolsRuntime)
103+
return
104+
105+
const devtoolsRule = nuxt.options.routeRules['/__nuxt_devtools__/**']
106+
nuxt.options.routeRules['/__nuxt_devtools__/**'] = {
107+
...devtoolsRule,
108+
headers: {
109+
...wildCardRules?.headers,
110+
...securityHeaders,
111+
...devtoolsRule?.headers,
112+
'X-Frame-Options': 'SAMEORIGIN',
87113
},
88114
}
89115
},
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
const { useNuxt } = vi.hoisted(() => ({
4+
useNuxt: vi.fn(),
5+
}))
6+
7+
vi.mock('nuxt/kit', () => ({
8+
defineNuxtModule: <T>(module: T) => module,
9+
useNuxt,
10+
}))
11+
12+
import securityHeadersModule from '../../../modules/security-headers'
13+
14+
type RouteRule = {
15+
headers?: Record<string, string>
16+
redirect?: string
17+
}
18+
19+
type MockNuxt = {
20+
options: {
21+
app: {
22+
head?: {
23+
meta?: Array<Record<string, string>>
24+
}
25+
}
26+
dev: boolean
27+
devtools?: boolean | { enabled?: boolean }
28+
routeRules: Record<string, RouteRule>
29+
}
30+
}
31+
32+
function createNuxt(options: Partial<MockNuxt['options']> = {}): MockNuxt {
33+
return {
34+
options: {
35+
app: {},
36+
dev: false,
37+
devtools: false,
38+
routeRules: {},
39+
...options,
40+
},
41+
}
42+
}
43+
44+
function getCsp(nuxt: MockNuxt) {
45+
return nuxt.options.app.head?.meta?.find(
46+
meta => meta['http-equiv'] === 'Content-Security-Policy',
47+
)?.content
48+
}
49+
50+
describe('security headers module', () => {
51+
beforeEach(() => {
52+
delete process.env.TEST
53+
useNuxt.mockReset()
54+
})
55+
56+
it('keeps security headers and only relaxes devtools-specific bits in dev', () => {
57+
const nuxt = createNuxt({
58+
dev: true,
59+
devtools: { enabled: true },
60+
routeRules: {
61+
'/**': {
62+
headers: {
63+
'Permissions-Policy': 'camera=()',
64+
},
65+
},
66+
'/__nuxt_devtools__/**': {
67+
headers: {
68+
'Cache-Control': 'no-store',
69+
},
70+
redirect: '/devtools',
71+
},
72+
},
73+
})
74+
75+
useNuxt.mockReturnValue(nuxt)
76+
securityHeadersModule.setup()
77+
78+
const csp = getCsp(nuxt)
79+
80+
expect(csp).toContain('ws://localhost:*')
81+
expect(csp).toContain("frame-src https://bsky.app https://pdsmoover.com 'self'")
82+
expect(nuxt.options.routeRules['/**']?.headers).toEqual(expect.objectContaining({
83+
'Permissions-Policy': 'camera=()',
84+
'Referrer-Policy': 'strict-origin-when-cross-origin',
85+
'X-Content-Type-Options': 'nosniff',
86+
'X-Frame-Options': 'DENY',
87+
}))
88+
expect(nuxt.options.routeRules['/__nuxt_devtools__/**']).toEqual({
89+
headers: {
90+
'Cache-Control': 'no-store',
91+
'Permissions-Policy': 'camera=()',
92+
'Referrer-Policy': 'strict-origin-when-cross-origin',
93+
'X-Content-Type-Options': 'nosniff',
94+
'X-Frame-Options': 'SAMEORIGIN',
95+
},
96+
redirect: '/devtools',
97+
})
98+
})
99+
100+
it('does not apply devtools relaxations when devtools are disabled via object config', () => {
101+
const nuxt = createNuxt({
102+
dev: true,
103+
devtools: { enabled: false },
104+
})
105+
106+
useNuxt.mockReturnValue(nuxt)
107+
securityHeadersModule.setup()
108+
109+
const csp = getCsp(nuxt)
110+
111+
expect(csp).not.toContain('ws://localhost:*')
112+
expect(csp).not.toContain("frame-src https://bsky.app https://pdsmoover.com 'self'")
113+
expect(nuxt.options.routeRules['/__nuxt_devtools__/**']).toBeUndefined()
114+
})
115+
})

0 commit comments

Comments
 (0)