I scanned the top 100 websites on the Tranco list last week. You know how many had all recommended security headers?
Twelve.
The other 88 were missing at least one critical security header that takes 2 minutes to add.
What Are Security Headers?
Security headers are HTTP response headers that tell browsers how to behave when handling your site's content. They prevent XSS, clickjacking, MIME sniffing, and other common attacks.
Here are the 5 most important ones — and how to add each in under 2 minutes.
1. Content-Security-Policy (CSP)
What it prevents: Cross-Site Scripting (XSS), data injection attacks
Missing from: 72% of websites in my scan
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'
Add it:
Express.js:
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));
Nginx:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
Next.js (next.config.js):
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'"
}
];
module.exports = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
}
};
2. Strict-Transport-Security (HSTS)
What it prevents: Protocol downgrade attacks, cookie hijacking
Missing from: 34% of HTTPS websites
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
This tells browsers to ONLY connect via HTTPS for the next year. Without it, the first request might be HTTP — vulnerable to man-in-the-middle attacks.
Add it:
// Express.js
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true }));
# Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
3. X-Content-Type-Options
What it prevents: MIME type sniffing attacks
Missing from: 41% of websites
X-Content-Type-Options: nosniff
Without this header, browsers might interpret a file as a different MIME type than declared. An attacker could upload a .jpg that's actually JavaScript — and the browser would execute it.
Add it:
app.use(helmet.noSniff());
add_header X-Content-Type-Options "nosniff" always;
4. X-Frame-Options
What it prevents: Clickjacking attacks
Missing from: 38% of websites
X-Frame-Options: DENY
This prevents your site from being embedded in an iframe. Without it, attackers can overlay invisible buttons on your site and trick users into clicking them.
Add it:
app.use(helmet.frameguard({ action: 'deny' }));
add_header X-Frame-Options "DENY" always;
5. Permissions-Policy
What it prevents: Unauthorized access to browser features (camera, microphone, geolocation)
Missing from: 89% of websites
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
This explicitly disables browser features your site doesn't need. The interest-cohort=() part opts out of Google's FLoC tracking.
Add it:
app.use(helmet.permittedCrossDomainPolicies());
// Or manually:
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
The All-in-One Solution
If you're using Express.js, just install Helmet:
npm install helmet
const helmet = require('helmet');
app.use(helmet()); // Adds ALL security headers with sane defaults
That's it. One line. All 5 headers (and more) are set.
Check Your Site Right Now
# Quick check any website's security headers
curl -sI https://yoursite.com | grep -iE 'content-security|strict-transport|x-content-type|x-frame|permissions-policy'
Or use these free online tools:
- securityheaders.com
- observatory.mozilla.org
A Script to Audit Multiple Sites
import requests
def check_security_headers(url):
headers_to_check = [
'Content-Security-Policy',
'Strict-Transport-Security',
'X-Content-Type-Options',
'X-Frame-Options',
'Permissions-Policy'
]
try:
resp = requests.head(url, timeout=10, allow_redirects=True)
except requests.RequestException as e:
return {"url": url, "error": str(e)}
results = {}
for header in headers_to_check:
results[header] = header in resp.headers
score = sum(results.values())
grade = {5: 'A+', 4: 'A', 3: 'B', 2: 'C', 1: 'D', 0: 'F'}
print(f"{url}: {grade.get(score, 'F')} ({score}/5 headers)")
for header, present in results.items():
status = 'OK' if present else 'MISSING'
print(f" [{status}] {header}")
return results
# Check your sites
sites = [
"https://example.com",
"https://yourapp.com",
]
for site in sites:
check_security_headers(site)
print()
How many security headers does your site have? Run the curl command above and share your score!
I build security tools for developers — check out awesome-devsec-tools for a full toolkit.
Follow for weekly security tips.
Top comments (0)