Skip to content

Security Headers

When to Use

Configure security headers on ALL HTTP responses. Headers provide defense-in-depth against XSS, clickjacking, MIME sniffing, and other attacks. As of 2025, X-Content-Type-Options leads adoption at ~50%, with HSTS and X-Frame-Options at ~35%.

Decision

Header Purpose Recommended Value
Content-Security-Policy Prevent XSS by restricting script sources default-src 'self'; script-src 'nonce-{random}'
Strict-Transport-Security (HSTS) Force HTTPS connections max-age=31536000; includeSubDomains; preload
X-Content-Type-Options Prevent MIME sniffing nosniff
X-Frame-Options Prevent clickjacking DENY or SAMEORIGIN
Referrer-Policy Control referer information strict-origin-when-cross-origin
Permissions-Policy Disable unnecessary browser features geolocation=(), microphone=(), camera=()
Cross-Origin-Opener-Policy (COOP) Isolate browsing context same-origin
Cross-Origin-Resource-Policy (CORP) Block cross-origin resource loading same-origin

Content-Security-Policy (CSP)

# Strict CSP with nonces (recommended 2025+)
Content-Security-Policy: default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none';

# Moderate CSP (if nonces not feasible)
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;

Major 2026 enforcement: SharePoint Online (March 2026) and Microsoft Entra ID (October 2026) enforcing CSP.

Strict-Transport-Security (HSTS)

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

HSTS preload list: Submit your domain to https://hstspreload.org/ for hardcoded HTTPS in browsers.

Warning: HSTS with preload is irreversible for months. Test thoroughly first with shorter max-age.

X-Content-Type-Options

X-Content-Type-Options: nosniff

Prevents MIME sniffing attacks where browsers might execute uploaded files as scripts.

X-Frame-Options

X-Frame-Options: DENY

Clickjacking attack scenario:

<!-- evil.com embeds your site in invisible iframe -->
<iframe src="https://yourbank.com/transfer" style="opacity:0"></iframe>
<button style="position:absolute;">Click to win!</button>
<!-- User clicks "win" button, actually clicks "transfer" in hidden iframe -->

Modern alternative — CSP frame-ancestors:

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

Referrer-Policy

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

Controls referer information sent with requests. Prevents leaking sensitive URL paths/parameters to external sites.

Permissions-Policy

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

Cross-Origin Policies

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin

Pattern

Complete security headers (Flask):

@app.after_request
def add_security_headers(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'nonce-{nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';"
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
    response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
    response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
    response.headers['Cross-Origin-Resource-Policy'] = 'same-origin'
    return response

Complete security headers (Nginx):

add_header Content-Security-Policy "default-src 'self'; script-src 'nonce-$request_id' 'strict-dynamic'; object-src 'none'; base-uri 'none';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 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=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;

Testing Security Headers

  • https://securityheaders.com — Grades A-F based on headers
  • https://observatory.mozilla.org — Mozilla Observatory
  • https://csp-evaluator.withgoogle.com — CSP validator

Common Mistakes

  • CSP with 'unsafe-inline' or 'unsafe-eval' — Defeats the purpose. Use nonces or hashes instead
  • HSTS without HTTPS — Can't set HSTS over HTTP. Deploy HTTPS first
  • X-Frame-Options and CSP frame-ancestors conflict — Use CSP frame-ancestors (more flexible)
  • Setting headers only on HTML pages — Set on ALL responses (CSS, JS, images, API)
  • Permissive CSP in productionscript-src * allows everything. Start strict, widen only as needed
  • Not testing headers — Use securityheaders.com to verify
  • Forgetting 'always' flag in Nginx — Without always, Nginx skips headers on error responses

See Also