Content Security Policy (CSP) is like a bouncer for your website. It tells the browser: "Only allow content (scripts, images, etc.) from these approved sources." This prevents malicious code from running on your site.
Short Form | Full Name | What It Does |
---|---|---|
CSP | Content Security Policy | Controls what resources can load on your page |
XSS | Cross-Site Scripting | Malicious scripts injected into your site |
CSRF | Cross-Site Request Forgery | Tricks users into making unwanted requests |
CORS | Cross-Origin Resource Sharing | Controls API access between different domains |
COEP | Cross-Origin-Embedder-Policy | Controls embedding content from other sites |
COOP | Cross-Origin-Opener-Policy | Controls popup/window interactions |
CORP | Cross-Origin-Resource-Policy | Controls who can embed your resources |
HSTS | HTTP Strict Transport Security | Forces HTTPS connections |
Directive | Controls | Example |
---|---|---|
default-src |
Fallback for everything | 'self' = only your own domain |
script-src |
JavaScript files | Where scripts can load from |
script-src-elem |
<script> tags specifically |
More specific than script-src |
style-src |
CSS files | Where stylesheets can load from |
img-src |
Images | Where images can load from |
connect-src |
API calls, websockets | Where your code can make requests to |
frame-src |
Iframes, embeds | What you can embed (YouTube, etc.) |
font-src |
Web fonts | Where fonts can load from |
The browser checks the MOST SPECIFIC rule first:
Priority | Directive | When It Applies |
---|---|---|
🥇 Highest | script-src-elem |
<script> tags |
🥈 | script-src-attr |
onclick="..." attributes |
🥉 Fallback | script-src |
All JavaScript |
🏁 Last Resort | default-src |
If nothing else matches |
Translation: If you have both script-src
and script-src-elem
, the browser ignores script-src
for <script>
tags.
Keyword | Meaning | Security Level |
---|---|---|
'self' |
Your own domain only | ✅ Secure |
'none' |
Block everything | ✅ Very Secure |
'unsafe-inline' |
Allow inline code | |
'unsafe-eval' |
Allow eval() functions | |
https: |
Any HTTPS site | ❌ Too Permissive |
data: |
Base64 embedded content |
Header | What Attack It Prevents | What It Does |
---|---|---|
X-Frame-Options | Clickjacking | Stops others from embedding your site in frames |
X-Content-Type-Options | MIME sniffing attacks | Forces browser to trust your file type declarations |
X-XSS-Protection | Cross-site scripting | Browser's built-in XSS filter (mostly legacy) |
Referrer-Policy | Information leakage | Controls what info is sent when users click links |
Permissions-Policy | Feature abuse | Controls access to camera, microphone, location, etc. |
Pattern | Matches | Example |
---|---|---|
https://example.com |
Exact domain only | Just example.com |
https://*.example.com |
Any subdomain | api.example.com, cdn.example.com |
https: |
Any HTTPS site | |
'self' |
Your current domain | Whatever domain the page is on |
- Look at the error message - it tells you exactly what failed
- Identify the directive - which rule was violated?
- Add the domain to the right place - don't just add it everywhere
- Test locally first - don't wait for deployment
Error Contains | Add Domain To | Why |
---|---|---|
"script" | script-src-elem |
JavaScript loading |
"img-src" | img-src |
Image loading |
"connect-src" | connect-src |
API calls, websockets |
"frame-src" | frame-src |
YouTube embeds, iframes |
Think of CSP like airport security:
default-src 'self'
= "Only people from this terminal"script-src 'https://cdn.example.com'
= "Also allow people from this specific airline"'unsafe-inline'
= "Let anyone through without checking"⚠️
The goal: Allow what you need, block everything else.
✅ Start strict, then add permissions as needed
✅ Test in development with CSP enabled
✅ Be specific - avoid wildcards when possible
✅ Check browser console for violation reports
❌ Don't disable CSP entirely
❌ Don't use 'unsafe-inline'
unless absolutely necessary
❌ Don't add domains to every directive - be precise
contentSecurityPolicy: {
'default-src': [
"'self'",
"https://embed.tawk.to",
"https://*.youtube.com",
"https://*.google-analytics.com"
// ... all your domains
]
}
What happens:
- ✅ Scripts work from all those domains
- ✅ Images work from all those domains
- ✅ Styles work from all those domains
- ✅ API calls work to all those domains
- ✅ Frames work from all those domains
The problem: This is overly permissive! You're basically saying "YouTube can load scripts, images, make API calls, AND embed frames on my site" when YouTube probably only needs to embed videos.
contentSecurityPolicy: {
'default-src': ["'self'"], // Only your domain for everything else
'script-src-elem': [
"'self'",
"https://embed.tawk.to",
"https://*.youtube.com",
"https://*.google-analytics.com"
// ... all your domains
]
}
What happens:
- ✅ Scripts work from all those domains
- ❌ Images FAIL from external domains (falls back to
default-src: 'self'
) - ❌ Styles FAIL from external domains (falls back to
default-src: 'self'
) - ❌ API calls FAIL to external domains (falls back to
default-src: 'self'
) - ❌ Frames FAIL from external domains (falls back to
default-src: 'self'
)
The problem: You'll get errors like:
Refused to load image 'https://youtube.com/thumbnail.jpg' because it violates img-src directive
Refused to display 'https://youtube.com/embed/video' in a frame because it violates frame-src directive
Let's say you have a YouTube embed on your page:
With everything in default-src
:
<!-- This works, but gives YouTube too much power -->
<iframe src="https://youtube.com/embed/video123"></iframe>
<img src="https://youtube.com/thumbnail.jpg">
<script src="https://youtube.com/analytics.js"></script>
With everything in script-src-elem
only:
✅ <script src="https://youtube.com/analytics.js"></script> <!-- Works -->
❌ <iframe src="https://youtube.com/embed/video123"></iframe> <!-- Blocked -->
❌ <img src="https://youtube.com/thumbnail.jpg"> <!-- Blocked -->
Be specific about what each domain actually needs:
contentSecurityPolicy: {
'default-src': ["'self'"],
// Scripts: Only domains that actually serve JavaScript
'script-src-elem': ["'self'", "https://embed.tawk.to", "https://*.google-analytics.com"],
// Images: Domains that serve images/thumbnails
'img-src': ["'self'", "https://*.youtube.com", "https://*.googleapis.com"],
// Frames: Only YouTube for video embeds
'frame-src': ["https://*.youtube.com"],
// API calls: Only what your code actually connects to
'connect-src': ["'self'", "wss://embed.tawk.to", "https://*.google-analytics.com"]
}
This follows the principle of least privilege - each domain only gets the minimum permissions it needs to function.
Want to see this in action? Try putting everything in default-src
first - your site will work but be less secure. Then move domains to specific directives and watch certain things break until you put them in the right place. It's a great way to learn what each service actually needs!
// nuxt.config.ts - Complete Nuxt Security Configuration Guide
export default defineNuxtConfig({
// ===========================================
// BASE SECURITY CONFIG (applies to all environments)
// ===========================================
security: {
strict: true, // Enables all security features by default
rateLimiter: {
// API rate limiting (optional)
tokensPerInterval: 150,
interval: 300000, // 5 minutes
headers: false, // Don't expose rate limit in headers
},
headers: {
// CONTENT SECURITY POLICY - The main security layer
contentSecurityPolicy: {
// Baseline - everything falls back to this
'default-src': ["'self'"],
// Scripts - BE VERY CAREFUL HERE
'script-src-elem': [
"'self'",
// Add domains that serve JavaScript files ONLY
// DON'T add image or frame domains here
],
// Styles - Usually need 'unsafe-inline' for CSS-in-JS
'style-src': [
"'self'",
"'unsafe-inline'" // Most apps need this for dynamic styles
],
// Images - Can be more permissive
'img-src': [
"'self'",
'data:', // Base64 images
'blob:', // Generated images
// Add CDNs and image services here
],
// API calls and websockets
'connect-src': [
"'self'",
// Add your API endpoints here
// Add websocket endpoints (wss://)
],
// Embedded content (YouTube, maps, etc.)
'frame-src': [
// Only add domains you actually embed
// Don't add unless you use iframes
],
// Web fonts
'font-src': [
"'self'",
'data:', // Inline fonts
// Add font CDNs like Google Fonts
],
// Audio/Video
'media-src': ["'self'"],
// Web Workers (if you use them)
'worker-src': ["'self'"],
// Forms - where forms can submit to
'form-action': ["'self'"],
// Base URI for relative URLs
'base-uri': ["'self'"],
// Manifest files
'manifest-src': ["'self'"]
},
// CROSS-ORIGIN POLICIES
crossOriginEmbedderPolicy: 'unsafe-none', // or 'require-corp' for stricter
crossOriginOpenerPolicy: 'same-origin-allow-popups', // Allows OAuth popups
crossOriginResourcePolicy: 'same-origin', // or 'cross-origin' if needed
// LEGACY SECURITY HEADERS (still useful)
xContentTypeOptions: 'nosniff', // Prevent MIME sniffing
xDNSPrefetchControl: 'off', // Disable DNS prefetching
xDownloadOptions: 'noopen', // IE download security
xFrameOptions: 'SAMEORIGIN', // Prevent clickjacking (CSP frame-ancestors is better)
xPermittedCrossDomainPolicies: 'none', // Block Flash/PDF policies
xXSSProtection: '1; mode=block', // Legacy XSS protection
// REFERRER POLICY - Controls what info is sent in referrer header
referrerPolicy: 'strict-origin-when-cross-origin',
// PERMISSIONS POLICY - Controls browser features
permissionsPolicy: {
camera: [], // Block camera access
microphone: [], // Block microphone
geolocation: [], // Block location
'picture-in-picture': [], // Allow PiP if needed
fullscreen: ["'self'"], // Allow fullscreen on your domain
'payment': [], // Block payment API
'usb': [], // Block USB API
// Add others as needed: 'accelerometer', 'gyroscope', etc.
},
// STRICT TRANSPORT SECURITY - Force HTTPS
strictTransportSecurity: {
maxAge: 31536000, // 1 year
includeSubdomains: true,
preload: true // Submit to browser preload lists
}
},
// SSG-specific settings (for static generation)
ssg: {
meta: true, // Add security meta tags
hashScripts: true, // Add integrity hashes to scripts
hashStyles: false, // Usually causes issues with dynamic styles
nitroHeaders: true, // Add headers at build time
exportToPresets: true // Export security config to preset files
},
// REQUEST SIZE LIMITS
requestSizeLimiter: {
maxRequestSizeInBytes: 2000000, // 2MB
maxUploadFileRequestInBytes: 8000000, // 8MB
},
// HIDE SERVER INFO
hidePoweredBy: true, // Remove X-Powered-By header
// BASIC AUTH (for staging environments)
basicAuth: false // Enable in staging config below
},
// ===========================================
// PRODUCTION ENVIRONMENT - STRICTEST SECURITY
// ===========================================
$production: {
security: {
headers: {
contentSecurityPolicy: {
'default-src': ["'self'"],
// Production scripts - be VERY specific
'script-src-elem': [
"'self'",
// Analytics
'https://www.googletagmanager.com',
'https://*.googletagmanager.com',
'https://www.google-analytics.com',
'https://*.google-analytics.com',
// Ads (if needed)
'https://pagead2.googlesyndication.com',
'https://*.googlesyndication.com',
'https://googleads.g.doubleclick.net',
'https://*.doubleclick.net',
// Third-party services
'https://embed.tawk.to', // Chat widget
'https://static.cloudflareinsights.com', // Cloudflare analytics
// Your CDNs/services
// 'https://cdn.yourservice.com',
],
'style-src': [
"'self'",
"'unsafe-inline'", // Usually needed
// Add CSS CDNs here
// 'https://fonts.googleapis.com',
],
'img-src': [
"'self'",
'data:',
'blob:',
// Image CDNs and services
'https://*.googleapis.com',
'https://*.youtube.com', // YouTube thumbnails
'https://*.google-analytics.com',
// 'https://cdn.yourservice.com',
],
'connect-src': [
"'self'",
// Analytics endpoints
'https://www.google-analytics.com',
'https://*.google-analytics.com',
'https://analytics.google.com',
// Websockets
'wss://embed.tawk.to', // Tawk.to chat
// Your APIs
// 'https://api.yourservice.com',
],
'frame-src': [
// Only what you actually embed
'https://*.youtube.com',
'https://*.youtube-nocookie.com',
// Google Ads iframes (if needed)
'https://*.googlesyndication.com',
],
'font-src': [
"'self'",
'data:',
'https://fonts.gstatic.com', // Google Fonts
]
},
// Strictest transport security
strictTransportSecurity: {
maxAge: 63072000, // 2 years
includeSubdomains: true,
preload: true
}
}
}
},
// ===========================================
// DEVELOPMENT ENVIRONMENT - LOOSER BUT STILL PROTECTIVE
// ===========================================
$development: {
security: {
// Keep most security enabled but allow dev tools
headers: {
contentSecurityPolicy: {
'default-src': ["'self'"],
'script-src-elem': [
"'self'",
"'unsafe-eval'", // Required for Vite HMR
"'unsafe-inline'", // Sometimes needed for dev tools
// Include all your production domains for testing
'https://www.googletagmanager.com',
'https://*.googletagmanager.com',
'https://embed.tawk.to',
'https://static.cloudflareinsights.com',
// ... all your production script sources
],
'style-src': [
"'self'",
"'unsafe-inline'", // Needed for CSS HMR
'blob:', // Vite injects styles as blobs
],
'img-src': [
"'self'",
'data:',
'blob:',
'https:', // More permissive for development
],
'connect-src': [
"'self'",
// Dev server connections
'ws://localhost:*', // Vite HMR websocket
'ws://127.0.0.1:*',
'http://localhost:*',
'https://localhost:*',
// Include production endpoints for testing
'https://www.google-analytics.com',
'wss://embed.tawk.to',
// ... your production connect sources
],
'frame-src': [
"'self'",
// Same as production
'https://*.youtube.com',
'https://*.youtube-nocookie.com',
],
'font-src': [
"'self'",
'data:',
'https:', // More permissive for dev
]
},
// Disable HSTS in development (allows HTTP)
strictTransportSecurity: false
}
}
},
// ===========================================
// STAGING ENVIRONMENT - MIDDLE GROUND
// ===========================================
$staging: {
security: {
// Same as production but with basic auth and some debugging
basicAuth: {
name: 'staging',
pass: 'your-staging-password',
enabled: true,
message: 'Staging Environment - Enter credentials'
},
headers: {
// Same CSP as production
contentSecurityPolicy: {
// ... copy from production but maybe add:
'report-uri': ['/csp-violation-report'], // CSP violation reporting
}
}
}
},
// ===========================================
// TEST ENVIRONMENT - MINIMAL SECURITY FOR E2E TESTS
// ===========================================
$test: {
security: {
// Disable rate limiting for tests
rateLimiter: false,
headers: {
// Very permissive CSP for testing
contentSecurityPolicy: {
'default-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
'script-src-elem': ["'self'", "'unsafe-inline'", "'unsafe-eval'", 'https:'],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'https:', 'blob:'],
'connect-src': ["'self'", 'https:', 'wss:', 'ws:'],
'frame-src': ['https:']
}
}
}
},
// ===========================================
// COMMON PATTERNS FOR SPECIFIC SERVICES
// ===========================================
// If you use Google Services, add this to your environment configs:
/*
Google Analytics + Tag Manager + Ads:
'script-src-elem': [
'https://www.googletagmanager.com',
'https://*.googletagmanager.com',
'https://www.google-analytics.com',
'https://*.google-analytics.com',
'https://pagead2.googlesyndication.com',
'https://*.googlesyndication.com',
'https://googleads.g.doubleclick.net',
'https://*.doubleclick.net'
],
'connect-src': [
'https://www.google-analytics.com',
'https://*.google-analytics.com',
'https://analytics.google.com'
],
'img-src': [
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://*.doubleclick.net'
],
'frame-src': [
'https://*.googlesyndication.com' // For ad iframes
]
*/
// If you use YouTube embeds:
/*
'frame-src': [
'https://*.youtube.com',
'https://*.youtube-nocookie.com'
],
'img-src': [
'https://*.youtube.com' // For thumbnails
]
*/
// If you use Stripe:
/*
'script-src-elem': ['https://js.stripe.com'],
'connect-src': ['https://api.stripe.com'],
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com']
*/
// If you use Google Fonts:
/*
'style-src': ['https://fonts.googleapis.com'],
'font-src': ['https://fonts.gstatic.com']
*/
})
Google recommends using strict CSP with nonces rather than domain allowlists because "the domains that Google Publisher Tag (GPT) uses change over time". However, if you must use domain-based CSP, here are the known requirements:
'script-src-elem': [
"'self'",
// Core AdSense/Ads domains
"https://pagead2.googlesyndication.com",
"https://partner.googleadservices.com",
"https://tpc.googlesyndication.com",
"https://www.googletagservices.com",
"https://securepubads.g.doubleclick.net", // GPT
// Analytics (if using)
"https://www.googletagmanager.com",
"https://*.googletagmanager.com"
]
'frame-src': [
"https://googleads.g.doubleclick.net",
"https://tpc.googlesyndication.com",
"https://*.googlesyndication.com"
]
'img-src': [
"'self'",
"https://pagead2.googlesyndication.com",
// Problem: Google uses geo-specific domains
"https://www.google.com",
"https://www.google.co.uk",
"https://www.google.ca",
// ... potentially hundreds more
]
'connect-src': [
"'self'",
"https://pagead2.googlesyndication.com"
]
Google uses different domains based on user location (google.com, google.co.uk, google.ca, etc.) which makes domain-based CSP very difficult. The community solutions are:
Option 1: Use https:
for images (less secure)
'img-src': ["'self'", "https:"]
Option 2: Add hundreds of country domains (from the search results)
'script-src-elem': [
"https://adservice.google.com",
"https://adservice.google.ad",
"https://adservice.google.ae",
"https://adservice.google.com.af",
// ... continue for all 200+ countries
]
Option 3: Google's Recommended Approach - Nonces
// Use nonces instead of domains
'script-src': ["'nonce-{random-value}'", "'unsafe-inline'", "'strict-dynamic'"]
- Google uses hundreds of domains for geographic optimization
- These domains change over time without notice
- Different Google services use different domain patterns
- Most documentation is incomplete or outdated
Start with the core domains above, then use browser dev tools to catch what you miss. Google's own recommendation is to use nonces instead of trying to maintain domain lists.