1. Library
  2. Computer Networks
  3. Http and the Web
  4. Headers

Updated 8 hours ago

Security headers exist because you don't trust your own code—and you shouldn't.

No matter how careful you are, your application might have vulnerabilities. Cross-site scripting, clickjacking, protocol downgrades—these attacks succeed when your code fails to prevent them. Security headers shift the enforcement from your application to the browser itself. Even if your code has a bug, the browser refuses to execute the attack.

That's the real insight: security headers are guardrails you install because you know you're fallible.

Strict-Transport-Security (HSTS)

HTTPS is meaningless if an attacker can trick the browser into using HTTP instead. HSTS makes HTTPS mandatory.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Once a browser sees this header, it refuses to connect over HTTP. Every request becomes HTTPS automatically. If the certificate is invalid, there's no bypass button—the browser simply won't connect.

The Directives

max-age sets how long the browser remembers this rule. 31,536,000 seconds is one year.

includeSubDomains extends the rule to all subdomains. Be certain every subdomain supports HTTPS before enabling this—you'll break any that don't.

preload lets you submit your domain to browser preload lists at hstspreload.org. These are domains hardcoded into browsers as HTTPS-only.

The First-Visit Problem

HSTS has an honest limitation: it can't protect the first visit. Before the browser receives the header, it doesn't know to use HTTPS. An attacker who intercepts that first connection can downgrade it to HTTP.

Preload lists exist because of this limitation. A domain on the preload list is protected even for first-time visitors—the rule is baked into the browser itself.

Content-Security-Policy (CSP)

CSP is the most powerful security header. It tells the browser exactly which resources are allowed to load and execute.

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'

If an attacker injects a malicious script into your page, CSP blocks its execution. The script doesn't match the allowed sources, so the browser refuses to run it.

The Core Directives

default-src is the fallback for any resource type you don't explicitly configure:

default-src 'self'

script-src controls where JavaScript can load from:

script-src 'self' https://trusted.cdn.com

style-src controls stylesheets:

style-src 'self' 'unsafe-inline'

img-src controls images:

img-src 'self' data: https:

connect-src controls fetch, XMLHttpRequest, WebSocket, and EventSource:

connect-src 'self' https://api.example.com

frame-ancestors controls who can embed your page in a frame:

frame-ancestors 'none'

Source Values

'none' blocks everything:

script-src 'none'

'self' allows your own origin:

script-src 'self'

'unsafe-inline' allows inline scripts—avoid this if possible:

script-src 'unsafe-inline'

'unsafe-eval' allows eval()—avoid this too:

script-src 'unsafe-eval'

nonce-[value] allows specific inline scripts that include a matching nonce:

script-src 'nonce-rAnd0m123'
<script nonce="rAnd0m123">
  // This script runs because the nonce matches
</script>

Domain sources allow specific origins:

script-src https://trusted.cdn.com

Starting Strict

Begin with a restrictive policy:

default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'

Then relax only what breaks. Use Report-Only mode to test without blocking:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

This logs violations without enforcing, so you can identify what your policy would break before deploying it.

X-Content-Type-Options

Browsers sometimes ignore what you tell them a file is and guess instead. An image might be interpreted as JavaScript. A text file might render as HTML.

X-Content-Type-Options: nosniff

This single directive tells the browser: trust the Content-Type header. Don't guess. If something says it's an image, treat it as an image—don't execute it as code.

X-Frame-Options

Clickjacking works by embedding your site in an invisible frame, then overlaying it on a malicious page. Users think they're clicking one thing but actually interact with your hidden site.

X-Frame-Options: DENY

DENY prevents all framing. SAMEORIGIN allows framing only by your own origin.

Modern applications should use CSP's frame-ancestors instead:

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

CSP provides more flexibility, and X-Frame-Options is effectively deprecated.

Referrer-Policy

When you click a link, your browser tells the destination where you came from. That referrer URL might contain sensitive information—tokens, session IDs, personal data in query strings.

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

This policy sends the full URL for same-origin requests but only the origin (not the path or query string) for cross-origin requests. It's the right balance for most applications.

Other options:

  • no-referrer: Never send referrer information
  • same-origin: Send referrer only for same-origin requests
  • strict-origin: Send only the origin, and nothing when downgrading to HTTP

Permissions-Policy

If your site doesn't use the camera, why should any script on your page be able to access it?

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

The empty parentheses mean no origin is allowed. Even if an attacker injects malicious code, they can't access these APIs.

To allow specific features for your own origin:

Permissions-Policy: geolocation=(self), payment=(self "https://trusted-payment.com")

Cross-Origin Isolation Headers

Three headers work together to isolate your page from cross-origin interference:

Cross-Origin-Embedder-Policy (COEP):

Cross-Origin-Embedder-Policy: require-corp

Resources must explicitly opt-in to being loaded by your page.

Cross-Origin-Opener-Policy (COOP):

Cross-Origin-Opener-Policy: same-origin

Isolates your browsing context from cross-origin pages.

Cross-Origin-Resource-Policy (CORP):

Cross-Origin-Resource-Policy: same-origin

Controls which origins can load your resources.

These headers enable powerful features like SharedArrayBuffer while protecting against Spectre-style attacks.

Implementation

Nginx:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'" 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;

Apache:

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"

Node.js with Helmet:

const helmet = require('helmet');
app.use(helmet());

Helmet sets sensible defaults for all major security headers.

Testing Your Headers

Use securityheaders.com or Mozilla Observatory to audit your configuration. Or check manually in browser DevTools: open the Network tab, click any request, and examine the Response Headers.

Start with HSTS and CSP—they provide the most protection. Add the others incrementally. Use Report-Only mode for CSP until you're confident in your policy.

Security headers are one of the few places where a few lines of configuration can prevent entire categories of attacks. The browser becomes your ally, enforcing rules even when your code forgets to.

Frequently Asked Questions About Security Headers

Was this page helpful?

😔
🤨
😃
Security Headers • Library • Connected