Skip to main content

Command Palette

Search for a command to run...

6 Security Headers Every Web App Needs

Learn: 6 Security Headers Every Web App Needs

Updated
9 min readView as Markdown
T

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

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

  1. Start with Report-Only Mode: Use Content-Security-Policy-Report-Only to test without breaking functionality
  2. Avoid 'unsafe-inline': Inline scripts are XSS magnets. Use nonces or hashes instead
  3. Use a CSP Generator: Tools like csper.io can help you build policies
  4. Monitor Violations: Set up a report-uri to 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

ValueDescriptionUse Case
DENYNo framing allowed, periodMost secure option for standard web apps
SAMEORIGINOnly your domain can frame your pagesWhen you need to embed your own pages
ALLOW-FROM uriSpecific domain can frame your pagesLegacy 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

PolicyWhat's SentBest For
no-referrerNothingMaximum privacy
no-referrer-when-downgradeFull URL on HTTPS, nothing on HTTPDefault browser behavior
originOnly the origin (domain)Balanced approach
origin-when-cross-originFull URL same-origin, origin only cross-originGood balance
same-originFull URL same-origin, nothing cross-originInternal analytics
strict-originOrigin only, nothing on HTTPS→HTTPSecurity-focused
strict-origin-when-cross-originFull URL same-origin, origin cross-origin, nothing on downgradeRecommended default
unsafe-urlAlways full URLAvoid (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 origin
  • feature=(self "https://trusted.com") - Enable for your origin and specific domains
  • feature=* - 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

HeaderPrimary ThreatDifficultyImpactBrowser Support
Content-Security-PolicyXSS, injection attacksMedium-HighVery High96%+
Strict-Transport-SecurityMITM attacksLowHigh97%+
X-Frame-OptionsClickjackingLowMedium99%+
X-Content-Type-OptionsMIME sniffingLowMedium99%+
Referrer-PolicyPrivacy leaksLowMedium97%+
Permissions-PolicyFeature abuseMediumMedium-High90%+

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'),