diff --git a/docs/content/2.security/1.headers.md b/docs/content/2.security/1.headers.md index 1bbdbc7f..f6ae638e 100644 --- a/docs/content/2.security/1.headers.md +++ b/docs/content/2.security/1.headers.md @@ -3,7 +3,7 @@ title: Headers description: '' --- -A set of **global** Nuxt `routeRules` that will add appriopriate security headers to your response that will make your application more secure by default. All headers can be overriden by using the module configuration. Check out all the available types [here](https://github.com/Baroshem/nuxt-security/blob/main/src/types.ts). +A set of **global** Nuxt `routeRules` that will add appropriate security headers to your response that will make your application more secure by default. All headers can be overriden by using the module configuration. Check out all the available types [here](https://github.com/Baroshem/nuxt-security/blob/main/src/types.ts). It will help you solve [this](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#use-appropriate-security-headers) security problem. @@ -72,10 +72,19 @@ export default defineNuxtConfig({ nonce: true, headers: { contentSecurityPolicy: { + 'style-src': [ + "'self'", // fallback value for older browsers, automatically removed if `strict-dynamic` is supported. + "'nonce-{{nonce}}'", + ], 'script-src': [ "'self'", // fallback value for older browsers, automatically removed if `strict-dynamic` is supported. "'nonce-{{nonce}}'", "'strict-dynamic'" + ], + 'script-src-attr': [ + "'self'", // fallback value for older browsers, automatically removed if `strict-dynamic` is supported. + "'nonce-{{nonce}}'", + "'strict-dynamic'" ] } } @@ -83,7 +92,7 @@ export default defineNuxtConfig({ }) ``` -This will add a `nonce` attribute to all ` diff --git a/test/fixtures/nonce/nuxt.config.ts b/test/fixtures/nonce/nuxt.config.ts index 3da3c58f..07158bfe 100644 --- a/test/fixtures/nonce/nuxt.config.ts +++ b/test/fixtures/nonce/nuxt.config.ts @@ -1,16 +1,6 @@ import MyModule from '../../../src/module' export default defineNuxtConfig({ - app: { - head: { - script: [ - { src: '/loader.js' }, - { src: '/api/generated-script' }, - { innerHTML: 'var inlineLiteral = \'\'' } - ] - } - }, - modules: [ MyModule ], @@ -31,6 +21,7 @@ export default defineNuxtConfig({ nonce: true, headers: { contentSecurityPolicy: { + 'style-src': ["'self'", "'nonce-{{nonce}}'"], 'script-src': [ "'self'", // backwards compatibility for older browsers that don't support strict-dynamic "'nonce-{{nonce}}'", diff --git a/test/fixtures/nonce/pages/use-head.vue b/test/fixtures/nonce/pages/use-head.vue index dfe03bc8..92e29751 100644 --- a/test/fixtures/nonce/pages/use-head.vue +++ b/test/fixtures/nonce/pages/use-head.vue @@ -3,11 +3,12 @@ diff --git a/test/fixtures/nonce/pages/with-inline-script.vue b/test/fixtures/nonce/pages/with-inline-script.vue new file mode 100644 index 00000000..d529a399 --- /dev/null +++ b/test/fixtures/nonce/pages/with-inline-script.vue @@ -0,0 +1,13 @@ + + + diff --git a/test/fixtures/nonce/pages/with-styling.vue b/test/fixtures/nonce/pages/with-styling.vue new file mode 100644 index 00000000..810c960a --- /dev/null +++ b/test/fixtures/nonce/pages/with-styling.vue @@ -0,0 +1,9 @@ + + + diff --git a/test/fixtures/nonce/public/external.js b/test/fixtures/nonce/public/external.js index ca203799..c9c08519 100644 --- a/test/fixtures/nonce/public/external.js +++ b/test/fixtures/nonce/public/external.js @@ -1,3 +1,3 @@ // this file is simulated to be an external file // it will be loaded by the loader.js -alert('hello from external') +console.log('hello from external') diff --git a/test/fixtures/nonce/public/favicon.ico b/test/fixtures/nonce/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/nonce/public/loader.js b/test/fixtures/nonce/public/loader.js index 228ac558..120ddf41 100644 --- a/test/fixtures/nonce/public/loader.js +++ b/test/fixtures/nonce/public/loader.js @@ -3,7 +3,7 @@ function loader() { script.src = 'external.js'; // add to the DOM - document.body.appendChild(script); + document.head.appendChild(script); } loader(); diff --git a/test/nonce.test.ts b/test/nonce.test.ts index 45413a01..f4c50ee4 100644 --- a/test/nonce.test.ts +++ b/test/nonce.test.ts @@ -7,11 +7,13 @@ describe('[nuxt-security] Nonce', async () => { rootDir: fileURLToPath(new URL('./fixtures/nonce', import.meta.url)) }) + const expectedNonceElements = 7 // 1 from app.vue/useHead, 6 for nuxt + it('injects `nonce` attribute in response', async () => { const res = await fetch('/') const cspHeaderValue = res.headers.get('content-security-policy') - const nonce = cspHeaderValue?.match(/'nonce-(.*?)'/)[1] + const nonce = cspHeaderValue?.match(/'nonce-(.*?)'/)![1] const text = await res.text() const elementsWithNonce = text.match(new RegExp(`nonce="${nonce}"`, 'g'))?.length ?? 0 @@ -19,7 +21,7 @@ describe('[nuxt-security] Nonce', async () => { expect(res).toBeDefined() expect(res).toBeTruthy() expect(nonce).toBeDefined() - expect(elementsWithNonce).toBe(9) + expect(elementsWithNonce).toBe(expectedNonceElements) }) it('does not renew nonce if mode is `check`', async () => { @@ -41,7 +43,7 @@ describe('[nuxt-security] Nonce', async () => { const res = await fetch('/use-head') const cspHeaderValue = res.headers.get('content-security-policy') - const nonce = cspHeaderValue?.match(/'nonce-(.*?)'/)[1] + const nonce = cspHeaderValue!.match(/'nonce-(.*?)'/)![1] const text = await res.text() const elementsWithNonce = text.match(new RegExp(`nonce="${nonce}"`, 'g'))?.length ?? 0 @@ -49,7 +51,7 @@ describe('[nuxt-security] Nonce', async () => { expect(res).toBeDefined() expect(res).toBeTruthy() expect(nonce).toBeDefined() - expect(elementsWithNonce).toBe(11) + expect(elementsWithNonce).toBe(expectedNonceElements + 1) // 1 extra for loader.js in useHead }) it('removes the nonce from the CSP header when nonce is disabled', async () => { @@ -59,15 +61,30 @@ describe('[nuxt-security] Nonce', async () => { const noncesInCsp = cspHeaderValue?.match(/'nonce-(.*?)'/)?.length ?? 0 expect(noncesInCsp).toBe(0) - expect(cspHeaderValue).toBe("base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'self' 'strict-dynamic'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests; script-src 'self' 'strict-dynamic'") + expect(cspHeaderValue).toBe("base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'self' 'strict-dynamic'; style-src 'self' ; upgrade-insecure-requests; script-src 'self' 'strict-dynamic'") }) it('does not add nonce to literal strings', async () => { - const res = await fetch('/') + const res = await fetch('/with-inline-script') const text = await res.text() - const untouchedLiteral = text.includes('var inlineLiteral = \'\'') + const untouchedLiteral = text.includes('var inlineLiteral = \'