diff --git a/src/utils/headers.ts b/src/utils/headers.ts index c04984ea..2af9c0cd 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -177,6 +177,7 @@ export function getHeadersApplicableToAllResources(headers: SecurityHeaders) { Object.entries(headers) .filter(([key]) => appliesToAllResources(key as OptionKey)) .map(([key, value]) => ([getNameFromKey(key as OptionKey), headerStringFromObject(key as OptionKey, value)])) + .filter(([, value]) => Boolean(value)) ) return Object.keys(applicableHeaders).length === 0 ? undefined : applicableHeaders } diff --git a/test/fixtures/publicAssets/.nuxtrc b/test/fixtures/publicAssets/.nuxtrc new file mode 100644 index 00000000..3c8c6a11 --- /dev/null +++ b/test/fixtures/publicAssets/.nuxtrc @@ -0,0 +1 @@ +imports.autoImport=true \ No newline at end of file diff --git a/test/fixtures/publicAssets/app.vue b/test/fixtures/publicAssets/app.vue new file mode 100644 index 00000000..2b1be090 --- /dev/null +++ b/test/fixtures/publicAssets/app.vue @@ -0,0 +1,5 @@ + diff --git a/test/fixtures/publicAssets/nuxt.config.ts b/test/fixtures/publicAssets/nuxt.config.ts new file mode 100644 index 00000000..0271bed5 --- /dev/null +++ b/test/fixtures/publicAssets/nuxt.config.ts @@ -0,0 +1,42 @@ +export default defineNuxtConfig({ + modules: [ + '../../../src/module' + ], + routeRules: { + '/test/**': { + security: { + headers: { + referrerPolicy: 'no-referrer', + strictTransportSecurity: { + maxAge: 15552000, + includeSubdomains: true, + }, + xContentTypeOptions: 'nosniff', + xDownloadOptions: 'noopen', + xFrameOptions: 'SAMEORIGIN', + xPermittedCrossDomainPolicies: 'none', + xXSSProtection: '0', + } + } + } + }, + security: { + headers: { + referrerPolicy: false, + strictTransportSecurity: false, + xContentTypeOptions: false, + xDownloadOptions: false, + xFrameOptions: false, + xPermittedCrossDomainPolicies: false, + xXSSProtection: false, + contentSecurityPolicy: { + 'script-src': [ + "'self'", + 'https:', + "'unsafe-inline'", + "'strict-dynamic'" + ] + } + } + } +}) diff --git a/test/fixtures/publicAssets/package.json b/test/fixtures/publicAssets/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/publicAssets/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/fixtures/publicAssets/pages/index.vue b/test/fixtures/publicAssets/pages/index.vue new file mode 100644 index 00000000..8371b274 --- /dev/null +++ b/test/fixtures/publicAssets/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/publicAssets/public/icon.png b/test/fixtures/publicAssets/public/icon.png new file mode 100644 index 00000000..dbbf391c Binary files /dev/null and b/test/fixtures/publicAssets/public/icon.png differ diff --git a/test/fixtures/publicAssets/public/test/icon.png b/test/fixtures/publicAssets/public/test/icon.png new file mode 100644 index 00000000..dbbf391c Binary files /dev/null and b/test/fixtures/publicAssets/public/test/icon.png differ diff --git a/test/publicAssets.test.ts b/test/publicAssets.test.ts new file mode 100644 index 00000000..9bfe2c70 --- /dev/null +++ b/test/publicAssets.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import { fileURLToPath } from 'node:url' +import { setup, fetch } from '@nuxt/test-utils' + +describe('[nuxt-security] Public Assets', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/publicAssets', import.meta.url)), + }) + + it('does not set all-resources security headers when disabled in config', async () => { + const { headers } = await fetch('/icon.png') + expect(headers).toBeDefined() + + // Security headers that are always set on all resources + const rp = headers.get('referrer-policy') + const sts = headers.get('strict-transport-security') + const xcto = headers.get('x-content-type-options') + const xdo = headers.get('x-download-options') + const xfo = headers.get('x-frame-options') + const xpcdp = headers.get('x-permitted-cross-domain-policies') + const xxp = headers.get('x-xss-protection') + + expect(rp).toBeNull() + expect(sts).toBeNull() + expect(xcto).toBeNull() + expect(xdo).toBeNull() + expect(xfo).toBeNull() + expect(xpcdp).toBeNull() + expect(xxp).toBeNull() + }) + + it('sets security headers on routes when specified in routeRules', async () => { + const { headers } = await fetch('/test') + expect(headers).toBeDefined() + + // Security headers that are always set on all resources + const rp = headers.get('referrer-policy') + const sts = headers.get('strict-transport-security') + const xcto = headers.get('x-content-type-options') + const xdo = headers.get('x-download-options') + const xfo = headers.get('x-frame-options') + const xpcdp = headers.get('x-permitted-cross-domain-policies') + const xxp = headers.get('x-xss-protection') + + expect(rp).toBe('no-referrer') + expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(xcto).toBe('nosniff') + expect(xdo).toBe('noopen') + expect(xfo).toBe('SAMEORIGIN') + expect(xpcdp).toBe('none') + expect(xxp).toBe('0') + }) +})