Missing Security Headers
What This Means
Security headers are HTTP response headers that tell browsers how to behave when handling your website's content, protecting against common attacks like cross-site scripting (XSS), clickjacking, code injection, and data theft. When security headers are missing or misconfigured, your site is vulnerable to attacks that can steal user data, deface your website, redirect users to malicious sites, inject malicious code, and damage your reputation.
Essential Security Headers
Basic Security Headers:
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
Permissions-Policy: geolocation=(), microphone=(), camera=()
What Each Header Does:
- X-Frame-Options - Prevents clickjacking attacks
- X-Content-Type-Options - Stops MIME-type sniffing
- X-XSS-Protection - Enables XSS filter (legacy browsers)
- Referrer-Policy - Controls referrer information
- Content-Security-Policy - Prevents XSS and code injection
- Strict-Transport-Security - Forces HTTPS connections
- Permissions-Policy - Controls browser features
Impact on Your Business
Security Risks Without Headers:
- XSS attacks - Hackers inject malicious scripts
- Clickjacking - Users tricked into clicking hidden elements
- Data theft - Sensitive information stolen
- Session hijacking - User accounts compromised
- Code injection - Malware inserted into pages
- Reputation damage - Site flagged as unsafe
Business Consequences:
- Customer trust loss - Users avoid "unsafe" sites
- SEO penalties - Google penalizes insecure sites
- Legal liability - GDPR/PCI DSS compliance failures
- Financial losses - Data breaches cost $4.24M average
- Brand damage - News of security breach spreads
- Downtime - Cleaning up after attacks takes time
Real-World Stats:
- 43% of cyberattacks target small businesses
- Sites without security headers are 5x more likely to be compromised
- Average cost of data breach: $4.24 million
- 60% of small businesses close within 6 months of cyber attack
How to Diagnose
Method 1: SecurityHeaders.com
- Visit SecurityHeaders.com
- Enter your domain
- Click Scan
Check Score:
Grade F (Bad):
❌ X-Frame-Options: Missing
❌ X-Content-Type-Options: Missing
❌ Content-Security-Policy: Missing
❌ Strict-Transport-Security: Missing
Grade A+ (Good):
✅ X-Frame-Options: SAMEORIGIN
✅ X-Content-Type-Options: nosniff
✅ Content-Security-Policy: default-src 'self'
✅ Strict-Transport-Security: max-age=31536000
Method 2: Browser DevTools
- Open DevTools (
F12) - Go to Network tab
- Reload page
- Click main document
- Check Headers → Response Headers
Look For:
❌ MISSING headers:
(No security headers present)
✅ GOOD headers:
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'
Method 3: curl Command
# Check security headers
curl -I https://example.com
# Output:
HTTP/2 200
x-frame-options: DENY
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'
# ✅ Headers present
# Or if missing:
HTTP/2 200
content-type: text/html
# ❌ No security headers
Method 4: Mozilla Observatory
- Visit Mozilla Observatory
- Enter your domain
- Click Scan Me
Review Results:
Score: 35/100 (F)
- Content Security Policy not implemented: -25
- X-Frame-Options not implemented: -20
- Strict-Transport-Security not implemented: -20
Method 5: Lighthouse Security Audit
- Open DevTools (
F12) - Go to Lighthouse tab
- Select Best Practices
- Run audit
Check For:
⚠️ Does not use HTTPS
⚠️ Includes front-end JavaScript libraries with known vulnerabilities
⚠️ Browser errors logged to console
General Fixes
Fix 1: Add All Essential Headers
Nginx configuration:
# nginx.conf or site config
server {
listen 443 ssl http2;
server_name example.com;
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME-type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Enable XSS filter (legacy browsers)
add_header X-XSS-Protection "1; mode=block" always;
# Referrer policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Force HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;
# Permissions Policy
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Rest of your config...
}
Apache (.htaccess):
<IfModule mod_headers.c>
# Prevent clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
# Prevent MIME-type sniffing
Header always set X-Content-Type-Options "nosniff"
# Enable XSS filter
Header always set X-XSS-Protection "1; mode=block"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Force HTTPS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Content Security Policy
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
# Permissions Policy
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
Fix 2: Configure Content Security Policy (CSP)
Start restrictive, gradually open:
# Level 1: Very strict (might break things)
Content-Security-Policy: default-src 'self'
# Level 2: Allow inline styles/scripts (testing phase)
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'
# Level 3: Add external resources as needed
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.google-analytics.com https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com
# Use report-only mode for testing
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
CSP Directives Explained:
default-src 'self' # Default policy for all resources
script-src 'self' domain.com # Where scripts can load from
style-src 'self' # Where styles can load from
img-src 'self' data: https: # Where images can load from
font-src 'self' # Where fonts can load from
connect-src 'self' # AJAX/WebSocket connections
frame-src 'self' # iframe sources
frame-ancestors 'self' # Who can embed this page
object-src 'none' # Disable plugins (Flash, Java)
base-uri 'self' # Restrict <base> tag
form-action 'self' # Where forms can submit to
upgrade-insecure-requests # Upgrade HTTP to HTTPS
Fix 3: Strict-Transport-Security (HSTS)
Force HTTPS for all connections:
# Basic HSTS
Strict-Transport-Security: max-age=31536000
# Include subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains
# Preload (submit to browser HSTS list)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Submit to HSTS Preload List:
- Visit hstspreload.org
- Enter your domain
- Check eligibility
- Submit for preloading
- Wait for inclusion (weeks to months)
Fix 4: Node.js/Express with Helmet
Use Helmet middleware:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Apply all helmet defaults
app.use(helmet());
// OR configure individually
app.use(helmet({
// Prevent clickjacking
frameguard: { action: 'sameorigin' },
// Prevent MIME-type sniffing
contentTypeOptions: { nosniff: true },
// HSTS
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// CSP
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://www.google-analytics.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
frameAncestors: ["'self'"]
}
},
// Referrer policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
app.get('/', (req, res) => {
res.send('Hello with security headers!');
});
Fix 5: WordPress Security Headers
Using plugin (easy way):
- Install WP Security Headers plugin
- Go to Settings → Security Headers
- Enable recommended headers
- Save changes
Manual (functions.php or custom plugin):
add_action('send_headers', 'add_security_headers');
function add_security_headers() {
// Prevent clickjacking
header('X-Frame-Options: SAMEORIGIN');
// Prevent MIME-type sniffing
header('X-Content-Type-Options: nosniff');
// XSS protection
header('X-XSS-Protection: 1; mode=block');
// Referrer policy
header('Referrer-Policy: strict-origin-when-cross-origin');
// HSTS (if using HTTPS)
if (is_ssl()) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
// CSP (adjust as needed)
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'");
}
Fix 6: Cloudflare Security Headers
Using Transform Rules:
- Cloudflare Dashboard → Rules → Transform Rules
- HTTP Response Header Modification
- Create new rule:
Rule name: Security Headers
When incoming requests match:
- All incoming requests
Then:
- Set static header: X-Frame-Options = SAMEORIGIN
- Set static header: X-Content-Type-Options = nosniff
- Set static header: X-XSS-Protection = 1; mode=block
- Set static header: Referrer-Policy = strict-origin-when-cross-origin
- Set static header: Permissions-Policy = geolocation=(), microphone=(), camera=()
Using Workers:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const response = await fetch(request);
const newHeaders = new Headers(response.headers);
// Add security headers
newHeaders.set('X-Frame-Options', 'SAMEORIGIN');
newHeaders.set('X-Content-Type-Options', 'nosniff');
newHeaders.set('X-XSS-Protection', '1; mode=block');
newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
newHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
newHeaders.set('Content-Security-Policy', "default-src 'self'");
newHeaders.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
Fix 7: Netlify/Vercel Headers
Netlify (_headers file in root):
/*
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com
Permissions-Policy: geolocation=(), microphone=(), camera=()
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
},
{
"key": "Content-Security-Policy",
"value": "default-src 'self'"
},
{
"key": "Permissions-Policy",
"value": "geolocation=(), microphone=(), camera=()"
}
]
}
]
}
Fix 8: Testing CSP Without Breaking Site
Use report-only mode:
# Test CSP without blocking anything
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# CSP violations will be reported but not blocked
Create reporting endpoint:
// Node.js CSP report handler
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring system
res.status(204).end();
});
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing security headers:
Test 1: SecurityHeaders.com
- Visit SecurityHeaders.com
- Enter your domain
- Should get A or A+ grade
Test 2: curl Check
curl -I https://example.com
# Should show all headers:
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'
Test 3: Browser DevTools
- Network tab → Headers
- Verify all security headers present
- No console errors from CSP blocks
Test 4: Mozilla Observatory
- Scan site
- Score should be 70+
- No critical issues
Common Mistakes
- CSP too strict - Breaks functionality, use report-only first
- Missing 'always' in Nginx - Headers only sent on 200 responses
- Conflicting headers - Multiple X-Frame-Options values
- Not testing thoroughly - Site breaks after deployment
- Forgetting subdomains - HSTS includeSubDomains needed
- HSTS without HTTPS - Must have valid SSL first
- Hardcoding domains in CSP - Use variables for environments
- Not monitoring CSP reports - Miss violations