6 Security Headers Every Web App Needs
Learn: 6 Security Headers Every Web App Needs
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
6 Security Headers Every Web App Needs: Production Hardening Guide
I'll never forget the day our startup's web app got flagged by a security audit. We thought we'd done everything right—encrypted passwords, sanitized inputs, the works. Then the consultant pulled up our response headers and said, "You're basically leaving your front door wide open."
Turns out, we were missing critical HTTP security headers. Those invisible guardians that sit between your users and potential attackers. The fix took less than an hour, but the lesson? Priceless.
If you're pushing a web app to production without proper security headers, you're not just risking your data—you're risking your users' trust. Let me show you the six headers that transformed our security posture from "vulnerable" to "hardened."
Table of Contents
- Why Security Headers Matter More Than You Think
- 1. Content-Security-Policy (CSP): Your XSS Bodyguard
- 2. Strict-Transport-Security (HSTS): HTTPS Enforcer
- 3. X-Frame-Options: Clickjacking Defense
- 4. X-Content-Type-Options: MIME-Type Protection
- 5. Referrer-Policy: Privacy Controller
- 6. Permissions-Policy: Feature Access Manager
- Security Headers Comparison Table
- How to Implement Security Headers in Different Frameworks
- Testing Your Security Headers
- Common Mistakes to Avoid
- FAQ
- Key Takeaways
- Conclusion
Why Security Headers Matter More Than You Think
Security headers are HTTP response headers that tell browsers how to behave when handling your site's content. Think of them as rules of engagement between your server and the client's browser.
Here's the reality: 87% of websites don't implement proper security headers (according to recent security scans). That's like having a state-of-the-art alarm system but leaving your windows unlocked.
These headers protect against:
- Cross-Site Scripting (XSS) attacks
- Clickjacking attempts
- Man-in-the-middle attacks
- Data injection vulnerabilities
- Privacy leaks
The best part? They're free, relatively easy to implement, and provide immediate protection. Let's dive into each one.
1. Content-Security-Policy (CSP): Your XSS Bodyguard
Content-Security-Policy is the heavyweight champion of security headers. It's your first line of defense against Cross-Site Scripting (XSS) attacks—one of the most common web vulnerabilities.
What It Does
CSP tells the browser which sources of content are trustworthy. If a script tries to load from an unauthorized source, the browser blocks it. Simple as that.
Real-World Impact
Remember the British Airways breach in 2018? Attackers injected malicious JavaScript that stole customer payment data. A properly configured CSP would have blocked that script from executing.
Implementation Example
Here's a basic CSP header that allows scripts only from your domain:
# Apache (.htaccess)
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
# Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
// Express.js (Node.js)
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"]
}
}));
# Django (settings.py)
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP Directives Breakdown
- default-src: Fallback for all resource types
- script-src: Controls JavaScript sources
- style-src: Controls CSS sources
- img-src: Controls image sources
- font-src: Controls font sources
- connect-src: Controls AJAX, WebSocket, and EventSource connections
- frame-ancestors: Controls who can embed your site in frames
Pro Tips for CSP
- Start with Report-Only Mode: Use
Content-Security-Policy-Report-Onlyto test without breaking functionality - Avoid 'unsafe-inline': Inline scripts are XSS magnets. Use nonces or hashes instead
- Use a CSP Generator: Tools like csper.io can help you build policies
- Monitor Violations: Set up a
report-urito catch policy violations
// CSP with reporting
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
reportUri: '/csp-violation-report'
}
}));
// Endpoint to receive reports
app.post('/csp-violation-report', (req, res) => {
console.log('CSP Violation:', req.body);
res.status(204).end();
});
2. Strict-Transport-Security (HSTS): HTTPS Enforcer
HSTS is like a bouncer that only lets HTTPS traffic through. Once a browser sees this header, it refuses to connect to your site over plain HTTP—even if the user types http:// in the address bar.
Why You Need It
Man-in-the-middle attacks often exploit that brief moment when a user first connects via HTTP before being redirected to HTTPS. HSTS eliminates that window of vulnerability.
Implementation Example
# Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
// Express.js
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
# Rails (config/environments/production.rb)
config.force_ssl = true
config.ssl_options = {
hsts: {
expires: 31536000,
subdomains: true,
preload: true
}
}
HSTS Parameters Explained
- max-age: How long (in seconds) browsers should remember to only use HTTPS.
31536000= 1 year - includeSubDomains: Applies HSTS to all subdomains
- preload: Allows inclusion in browser preload lists (more on this below)
The HSTS Preload List
Want maximum protection? Submit your domain to the HSTS preload list. Major browsers ship with this list, meaning they'll enforce HTTPS even on the very first visit.
Warning: This is a one-way door. Removing your domain from the preload list takes months. Make sure you're ready for full HTTPS commitment.
3. X-Frame-Options: Clickjacking Defense
Clickjacking is sneaky. Attackers embed your site in an invisible iframe, then trick users into clicking buttons they can't see. The result? Users unknowingly perform actions on your site.
How X-Frame-Options Protects You
This header tells browsers whether your site can be embedded in frames, iframes, or objects.
Implementation Example
# Apache
Header always set X-Frame-Options "DENY"
# Nginx
add_header X-Frame-Options "DENY" always;
// Express.js
app.use(helmet.frameguard({ action: 'deny' }));
// PHP
header('X-Frame-Options: DENY');
X-Frame-Options Values
| Value | Description | Use Case |
DENY | No framing allowed, period | Most secure option for standard web apps |
SAMEORIGIN | Only your domain can frame your pages | When you need to embed your own pages |
ALLOW-FROM uri | Specific domain can frame your pages | Legacy option (deprecated, use CSP instead) |
Modern Alternative: CSP frame-ancestors
The frame-ancestors directive in CSP is more flexible and widely supported:
// More control with CSP
app.use(helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'self'", "https://trusted-partner.com"]
}
}));
4. X-Content-Type-Options: MIME-Type Protection
This header prevents a security vulnerability called "MIME-type sniffing" or "content sniffing."
The Problem
Browsers sometimes try to be "helpful" by guessing a file's content type, ignoring what your server declares. An attacker could upload a file disguised as an image that's actually JavaScript, and the browser might execute it.
The Solution
Set X-Content-Type-Options: nosniff to tell browsers: "Trust my Content-Type headers. Don't guess."
Implementation Example
# Apache
Header set X-Content-Type-Options "nosniff"
# Nginx
add_header X-Content-Type-Options "nosniff" always;
// Express.js
app.use(helmet.noSniff());
// Go
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
}
Why This Matters
I once saw a file upload vulnerability exploited because the server said "image/jpeg" but the browser detected JavaScript and executed it. One header would have prevented the entire attack.
5. Referrer-Policy: Privacy Controller
Every time a user clicks a link on your site, their browser sends a Referer header (yes, it's misspelled in the spec) to the destination. This can leak sensitive information.
What Gets Leaked
Without proper referrer policy, you might expose:
- Session tokens in URLs
- Search queries
- Private page paths
- User tracking data
Implementation Example
# Apache
Header set Referrer-Policy "strict-origin-when-cross-origin"
# Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
// Express.js
app.use(helmet.referrerPolicy({
policy: 'strict-origin-when-cross-origin'
}));
<!-- HTML meta tag (fallback) -->
<meta name="referrer" content="strict-origin-when-cross-origin">
Referrer-Policy Values Compared
| Policy | What's Sent | Best For |
no-referrer | Nothing | Maximum privacy |
no-referrer-when-downgrade | Full URL on HTTPS, nothing on HTTP | Default browser behavior |
origin | Only the origin (domain) | Balanced approach |
origin-when-cross-origin | Full URL same-origin, origin only cross-origin | Good balance |
same-origin | Full URL same-origin, nothing cross-origin | Internal analytics |
strict-origin | Origin only, nothing on HTTPS→HTTP | Security-focused |
strict-origin-when-cross-origin | Full URL same-origin, origin cross-origin, nothing on downgrade | Recommended default |
unsafe-url | Always full URL | Avoid (privacy risk) |
My Recommendation
Use strict-origin-when-cross-origin. It provides good privacy protection while maintaining functionality for analytics and legitimate tracking.
6. Permissions-Policy: Feature Access Manager
Formerly known as Feature-Policy, this header controls which browser features your site (and embedded content) can access.
Why It's Critical
Modern browsers have powerful APIs: geolocation, camera, microphone, payment handlers. Without restrictions, malicious third-party scripts or iframes could abuse these features.
Implementation Example
# Apache
Header set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
# Nginx
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
// Express.js
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=()'
);
next();
});
Common Features to Control
# Comprehensive permissions policy
add_header Permissions-Policy "
geolocation=(self),
microphone=(),
camera=(),
payment=(self),
usb=(),
magnetometer=(),
gyroscope=(),
accelerometer=(),
ambient-light-sensor=(),
autoplay=(self),
encrypted-media=(self),
fullscreen=(self),
picture-in-picture=(self)
" always;
Permissions-Policy Syntax
feature=()- Disable for everyone (including your site)feature=(self)- Enable only for your originfeature=(self "https://trusted.com")- Enable for your origin and specific domainsfeature=*- Enable for everyone (not recommended)
Real-World Example
// E-commerce site that needs payment API
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'payment=(self), geolocation=(), microphone=(), camera=()'
);
next();
});
Security Headers Comparison Table
| Header | Primary Threat | Difficulty | Impact | Browser Support |
| Content-Security-Policy | XSS, injection attacks | Medium-High | Very High | 96%+ |
| Strict-Transport-Security | MITM attacks | Low | High | 97%+ |
| X-Frame-Options | Clickjacking | Low | Medium | 99%+ |
| X-Content-Type-Options | MIME sniffing | Low | Medium | 99%+ |
| Referrer-Policy | Privacy leaks | Low | Medium | 97%+ |
| Permissions-Policy | Feature abuse | Medium | Medium-High | 90%+ |
How to Implement Security Headers in Different Frameworks
Express.js (Node.js) - Complete Setup
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use helmet with custom configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
}));
// Additional custom headers
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
});
app.listen(3000);
Django (Python) - Complete Setup
# settings.py
# Install django-csp: pip install django-csp
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'csp.middleware.CSPMiddleware',
# ... other middleware
]
# Security settings
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# CSP settings
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
# Referrer Policy
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# Custom middleware for Permissions-Policy
# middleware.py
class PermissionsPolicyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
return response
Ruby on Rails - Complete Setup
```ruby
config/environments/production.rb
Rails.application.configure do
Force SSL
config.force_ssl = true
HSTS
config.ssl_options = { hsts: { expires: 31536000, subdomains: true, preload: true } }
Use secure_headers gem
Gemfile: gem 'secure_headers'
config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config| config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.referrer_policy = "strict-origin-when-cross-origin"
config.csp = { default_src: %w('self'), script_src: %w('self'), style_src: %w('self' 'unsafe-inline'), img_src: %w('self' data: https:), font_src: %w('self'), connect_src: %w('self'),