thanks for all the work you put into this module! We’re running OWASP ZAP (2.17.0) against a Nuxt app (Nuxt + Nitro, started via nuxi build && nuxi preview) in GitHub Actions and noticed that several security headers seem to be missing / inconsistent, even though they are configured via nuxt-security.
security: {
strict: false,
nonce: true, // Enables HTML nonce support in SSR mode
sri: true, // Enable Subresource Integrity
headers: {
contentSecurityPolicy: {
'default-src': ['\'self\'', `*.${appDomain}`, 'https://*.supabase.co'],
'script-src': [
'\'self\'',
'\'nonce-{{nonce}}\'',
'\'strict-dynamic\'',
'https://static.cloudflareinsights.com',
'https://challenges.cloudflare.com',
`*.${appDomain}`,
'https://*.supabase.co',
'https://www.googletagmanager.com',
'https://tagmanager.google.com',
'https://js.stripe.com',
'https://m.stripe.network',
],
'worker-src': ['\'self\'', 'blob:'],
'frame-src': [
'\'self\'',
'https://challenges.cloudflare.com',
'https://js.stripe.com',
'https://hooks.stripe.com',
],
'img-src': [
'\'self\'',
'blob:',
'data:',
`https://*.${appDomain}`,
'https://*.supabase.co',
'https://www.googletagmanager.com',
'https://q.stripe.com',
],
'connect-src': [
'\'self\'',
'https://*.cloudflare.com',
'https://cloudflareinsights.com',
`https://*.${appDomain}`,
'https://*.supabase.co',
'wss://*.supabase.co',
'https://*.sentry.io',
'https://www.googletagmanager.com',
'https://*.google.com',
'https://*.google-analytics.com',
'https://api.stripe.com',
'https://q.stripe.com',
'https://m.stripe.network',
'https://hooks.stripe.com',
],
'style-src': [
'\'self\'',
'\'unsafe-inline\'',
],
'font-src': [
'\'self\'',
'data:',
],
'object-src': ['\'none\''],
'script-src-attr': ['\'none\''],
'base-uri': ['\'none\''],
'form-action': ['\'self\''],
'upgrade-insecure-requests': true,
},
permissionsPolicy: {
'camera': [],
'microphone': [],
'geolocation': [],
'display-capture': [],
'fullscreen': ['self'],
'web-share': ['self'],
'payment': [],
'publickey-credentials-get': ['self'],
},
referrerPolicy: 'strict-origin-when-cross-origin',
strictTransportSecurity: {
maxAge: 31536000,
includeSubdomains: true,
preload: true,
},
},
requestSizeLimiter: {
maxRequestSizeInBytes: 2000000,
maxUploadFileRequestInBytes: 8000000,
throwError: true,
},
rateLimiter: {
ipHeader: 'cf-connecting-ip',
headers: true,
tokensPerInterval: 300,
interval: 300000,
throwError: true,
driver: rateLimitStorage === 'cloudflare-kv-binding'
? {
name: 'cloudflare-kv-binding',
options: {
binding: kvBindingName,
base: 'ourCustomNameHidden:security:rate-limiter:',
},
}
: {
name: 'lruCache',
},
whiteList: ['127.0.0.1'],
},
xssValidator: {
throwError: true,
},
corsHandler: {
origin: [`https://${appDomain}`, `https://www.${appDomain}`, `https://api.${appDomain}`],
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
preflight: {
statusCode: 204,
},
},
allowedMethodsRestricter: {
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
throwError: true,
},
hidePoweredBy: true,
basicAuth: false,
enabled: true,
csrf: false,
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/],
},
ssg: {
meta: true,
hashScripts: true,
hashStyles: false,
nitroHeaders: true,
exportToPresets: false,
},
},
routeRules: {
'/': { prerender: true },
'/coming-soon': { prerender: true, cache: { maxAge: 60 * 60 * 24 } }, // Cache for a day
'/maintenance': { prerender: true, cache: { maxAge: 60 * 60 * 24 } }, // Cache for a day
'/about': { prerender: true },
'/about/**': { prerender: true },
'/support': { prerender: true },
'/support/**': { ssr: false },
'/auth/**': { ssr: false },
'/career': { ssr: false },
'/career/jobs': { ssr: false },
'/career/jobs/**': { ssr: false },
'/contact': { ssr: true },
'/legal/**': { prerender: false, cache: { maxAge: 60 * 60 * 24 } }, // Cache for a day
'/media': { prerender: true, cache: { maxAge: 60 * 60 * 3 } }, // Cache for 3 hours
'/media/**': { ssr: true, cache: { maxAge: 60 * 60 * 3 } }, // Cache for 3 hours
'/images/**': { ssr: true, cache: { maxAge: 60 * 60 * 3 } }, // Cache for 3 hours
'/press-kit/**': { ssr: true, cache: { maxAge: 60 * 60 * 3 } }, // Cache for 3 hours
'/solutions/**': { prerender: true, cache: { maxAge: 60 * 60 } }, // Cache for 1 hour and prerender
'/solutions/custom-product': { ssr: true },
'/api/v1/careers/jobs': {
cache: { maxAge: 60 * 60 }, // Cache for 1 hour
},
},
I have also attatched the entire ZAP report in case you want to grab some details!
Hi Team,
thanks for all the work you put into this module! We’re running OWASP ZAP (2.17.0) against a Nuxt app (Nuxt + Nitro, started via
nuxi build && nuxi preview) in GitHub Actions and noticed that several security headers seem to be missing / inconsistent, even though they are configured vianuxt-security.Observed behavior (from ZAP full scan)
ZAP reports
Access-Control-Allow-Origin: *on multiple non-API routes like/legal/,/media/,/portals/,/press-kit/,/solutions/(request includedOrigin: null) which triggers “CORS Misconfiguration” and “Cross-Domain Misconfiguration”./(root) but present on other routesZAP flags “Content Security Policy (CSP) Header Not Set” for
http://localhost:3000andhttp://localhost:3000/, while other routes like/maintenancedo return a CSP header.ZAP reports “Permissions Policy Header Not Set” and also missing
Cross-Origin-Opener-Policy,Cross-Origin-Embedder-Policy,Cross-Origin-Resource-Policyon/and also on_nuxt/*.jsassets (flagged as “Insufficient Site Isolation Against Spectre Vulnerability”).x-powered-by: Nuxtstill presentZAP still detects
x-powered-by: Nuxt(e.g. on/maintenance).What we think is happening
It looks like some responses (especially
/and potentially pre-rendered/static responses +_nuxtassets) may bypass the header injection path (middleware/hook) innuxi preview, leading to missing headers.Separately, the
corsHandlerbehavior appears to fall back toAccess-Control-Allow-Origin: *whenOriginisnull/missing, even though we expect “omit CORS headers entirely” unless explicitly allowed.Questions / recommended approach?
corsHandlercan emitAccess-Control-Allow-Origin: *for HTML pages (esp. withOrigin: null), and what’s the recommended safe configuration to prevent that?nuxi preview?/and_nuxt/*in Nuxt/Nitro?routeRules.headersas a fallback for those paths?Our current
nuxt.config.tssecurity and routeRules blocks:I have also attatched the entire ZAP report in case you want to grab some details!
report_md_hidden.md
Thanks!