Your First Line of Defense

HTTP response headers are one of the most overlooked yet powerful security mechanisms available to developers. They sit in the HTTP layer, before the browser even parses your HTML, giving you the chance to enforce strict security policies at the protocol level. Unlike code-level protections, headers apply universally across your application - they're browser-native, require no JavaScript, and work even if an attacker injects malicious content into your DOM.

When a browser receives an HTTP response, it reads these headers and enforces constraints on what the page can do. A well-configured security header tells the browser: "don't load scripts from anywhere except my domain" or "never embed this page in another site's iframe." If the browser detects a violation, it either blocks the action or logs a report - often before the malicious code executes.

The headers we'll cover work together as layers. CSP handles script execution. HSTS forces encrypted connections. X-Frame-Options prevents clickjacking. Referrer-Policy controls what leaks in the Referer header. When stacked correctly, they form a hardened perimeter around your application.

Content-Security-Policy (CSP): The Gatekeeper

Content-Security-Policy is the most powerful and most complex header. It works by implementing a whitelist model: the browser will only execute or load resources from sources you explicitly allow. This prevents inline script injection, external script injection, and other resource-based attacks.

What CSP Prevents

  • Cross-Site Scripting (XSS): If an attacker injects `<script>evil()</script>` into your page, CSP will block it unless you've whitelisted inline scripts. Even better, you can use nonces or hashes to allow specific inline scripts while blocking injected ones.
  • Data Injection Attacks: An attacker might inject a form that steals credentials. CSP can restrict form submissions to specific domains via `form-action`.
  • Clickjacking via Script Execution: While X-Frame-Options prevents embedding, CSP can restrict what scripts run and what external resources load, making JavaScript-based clickjacking harder.
  • Malicious Stylesheets: Without `style-src` control, an attacker could inject CSS to hide security warnings or phish credentials.

CSP Directive Anatomy

A CSP header is a series of directives. Each directive controls a different resource type or behavior.

  • default-src: The fallback for any resource type not explicitly defined. Set this restrictively, then override with specific directives.
  • script-src: Where scripts can be loaded from. This is critical - it's your main XSS defense.
  • style-src: Where stylesheets come from. Also allows `unsafe-inline` if you need inline styles.
  • img-src: Where images are loaded from. Usually safe to be permissive, but data exfiltration attacks can abuse this.
  • connect-src: Where XMLHttpRequest, WebSocket, fetch, and beacon requests can go. Critical for API security.
  • frame-ancestors: Which sites can embed your page in an iframe. Similar to X-Frame-Options but more powerful.
  • form-action: Where form submissions can POST to. Prevents credential theft via injected forms.
  • base-uri: Which URLs can be used in `<base href>` tags. Prevents attackers from redirecting relative paths.
  • object-src: Where Flash and other plugins come from. Best set to `none` unless you need legacy support.
  • report-uri / report-to: Where CSP violations are reported. Send these to a logging service.

A Real Restrictive CSP

Here's a practical example for a modern web application:

Content-Security-Policy: default-src 'none'; script-src 'self' https://cdn.example.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'; report-uri https://reporting.example.com/csp

Breaking this down:

  • default-src 'none': Deny everything by default. This is the most secure starting point.
  • script-src 'self' https://cdn.example.com: Allow scripts from your own domain and a trusted CDN. No inline scripts, no data: URIs.
  • style-src 'self' https://fonts.googleapis.com: Stylesheets from your domain and Google Fonts. No unsafe-inline.
  • img-src 'self' https:: Images from your domain or any HTTPS source (prevents mixed content).
  • font-src 'self' https://fonts.gstatic.com: Fonts from your domain and Google's font CDN.
  • connect-src 'self' https://api.example.com: API calls only to your own domain or your API server.
  • frame-ancestors 'none': Your page cannot be embedded in any iframe, anywhere.
  • form-action 'self': Forms only POST to your own origin.
  • base-uri 'self': The <base> tag can only reference your own origin.
  • object-src 'none': No Flash, no Java applets.
  • report-uri: Violations go to your logging endpoint for monitoring.

Testing with Content-Security-Policy-Report-Only

Before enforcing CSP, use `Content-Security-Policy-Report-Only` to test it. The browser will report violations without blocking them. This lets you discover incompatibilities in your code - missing nonces, inline scripts, unauthorized CDNs - before you break the site.

Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self'; connect-src 'self' https://api.example.com; report-uri https://reporting.example.com/csp

Monitor the reports, fix the violations (usually by adding nonces to inline scripts or moving code to external files), then switch to the enforcing header once violations drop to zero.

Common CSP Mistakes

  • unsafe-inline: Allows all inline scripts and styles, defeating most of CSP's protection. Use nonces or hashes instead.
  • Wildcard sources (*) or https:: Allows resources from any domain. Defeats the whitelist model.
  • Forgetting font-src or img-src: If not set, they fall back to default-src. If you're permissive with default-src, fonts and images are unrestricted.
  • No report-uri: You're flying blind. You won't know about violations until users complain.
  • Too permissive early on: Start restrictive, then relax. You'll never know if you start wide open.

Strict-Transport-Security (HSTS): Force HTTPS

HSTS tells the browser: "Always connect to this domain over HTTPS." If a user types `http://example.com`, the browser will silently upgrade it to `https://example.com` before making the request. This defeats SSL stripping attacks and prevents accidental unencrypted connections.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000: Cache this policy for one year (in seconds). Adjust down if you're not ready for long-term HTTPS commitment.
  • includeSubDomains: Apply HSTS to all subdomains (api.example.com, cdn.example.com, etc.). Omit if you have legacy subdomains that don't support HTTPS.
  • preload: Opt into the HSTS Preload List, a list built into all major browsers of domains that require HTTPS. This protects against attacks on the first visit, even before the browser receives the header.

⚠️ Preload Warning: Submitting to the preload list is difficult to undo. Once in the list, your domain requires HTTPS everywhere for months or years. Only add `preload` if you're 100% certain you'll maintain HTTPS indefinitely.

X-Frame-Options vs frame-ancestors

X-Frame-Options is the older header. It has three values:

  • DENY: Never embed this page in any iframe.
  • SAMEORIGIN: Only allow embedding if the iframe and the page share the same origin.
  • ALLOW-FROM uri: Allow embedding only from a specific origin (deprecated in favor of CSP).

frame-ancestors (part of CSP) is the modern replacement. It's more flexible and is what modern browsers prefer:

Content-Security-Policy: frame-ancestors 'none';

or

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;

If you set both headers, CSP's `frame-ancestors` takes precedence. Use `frame-ancestors` if you're already sending CSP; use `X-Frame-Options` only if you're on older systems.

X-Content-Type-Options: Block MIME Sniffing

Without this header, older browsers will guess the MIME type of a resource based on its content, not its Content-Type header. An attacker could upload a JavaScript file as `image.jpg`, and Internet Explorer would execute it as a script. This header stops that:

X-Content-Type-Options: nosniff

This single directive is crucial for security. Always include it.

Referrer-Policy: Control Referer Leakage

When a user clicks a link or loads a resource, the browser sends the `Referer` header to the destination, revealing the URL of the current page. This leaks information:

  • If you're on a private URL (e.g., `/user/123/private-notes`), the Referer exposes that path to external sites.
  • If you're on a page with sensitive query parameters, those leak too.

Referrer-Policy controls what the browser sends. Key values:

  • no-referrer: Never send the Referer header. Most private, but breaks some analytics and referrer tracking.
  • strict-origin-when-cross-origin: Send only the origin (scheme + domain) for cross-origin requests, nothing for same-origin. Good balance of privacy and functionality.
  • same-origin: Send full URL to same-origin, nothing for cross-origin.
  • no-referrer-when-downgrade: Send full URL unless downgrading from HTTPS to HTTP (legacy).

For most applications, use:

Referrer-Policy: strict-origin-when-cross-origin

Permissions-Policy (formerly Feature-Policy)

Permissions-Policy controls access to browser features like geolocation, camera, microphone, and payment APIs. Disable what you don't need:

Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=()

This tells the browser: "This site doesn't use geolocation, camera, microphone, payment APIs, or USB. Block them." This prevents injected code from requesting these sensitive features, and it also sends a signal that the site doesn't access them.

Cross-Origin Headers: CORS, CORP, COEP, COOP

Cross-Origin Resource Sharing (CORS): Controls which external sites can make requests to your API. Set `Access-Control-Allow-Origin` to specific origins, not `*`, unless your API is truly public.

Cross-Origin-Resource-Policy (CORP): Prevents your resources from being loaded by other sites' pages. Set to `same-origin` to restrict to your own domain, or `cross-origin` if you intend to be embedded.

Cross-Origin-Embedder-Policy (COEP): Opt into a stricter cross-origin policy that requires all sub-resources to explicitly allow embedding. Necessary for accessing certain APIs like `SharedArrayBuffer`.

Cross-Origin-Opener-Policy (COOP): Isolates your window from cross-origin windows that might try to access it via `window.opener`. Prevents Spectre-like attacks. Set to `same-origin`.

A Hardened Security Header Stack

Here's what a modern, production-ready security header stack looks like in nginx config format:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'; report-uri https://reporting.example.com/csp" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;

Or as HTTP response headers:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'; report-uri https://reporting.example.com/csp
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin

Deploy this with Content-Security-Policy-Report-Only first, monitor reports, then switch to enforcement. Start with tighter restrictions, then relax only what you absolutely need.

Tools to Help

Configuring security headers is detailed work. Use these onlinedevtools.app utilities to streamline the process:

  • CSP Analyzer: Paste a CSP header and get a detailed breakdown of each directive, common mistakes flagged, and recommendations.
  • Hash Generator: Generate CSP hash values for inline scripts. This lets you whitelist specific inline code without allowing all inline scripts.

Related Reading

  • Hashing Guide - Understand hashing and how it's used in CSP to protect inline scripts.
  • JWT Guide - Secure token-based authentication, often used alongside CSP-protected APIs.