DEV Community

Ayi NEDJIMI
Ayi NEDJIMI

Posted on

How to Implement Security Headers That Actually Matter in 2026

How to Implement Security Headers That Actually Matter in 2026

Security headers have been on every web security checklist for years, yet a quick scan of production traffic shows most applications still ship with defaults. A missing Content-Security-Policy turns a minor XSS bug into a session theft; an absent Strict-Transport-Security leaves users exposed on the first HTTP redirect. These aren't theoretical risks — they're the path of least resistance for attackers who enumerate easy wins before going deeper.

This article focuses on the headers that carry real weight, with working code you can drop into a Python or Go service today.

The Short List: Headers That Move the Needle

Not every security header deserves the same attention. Stripping X-Powered-By is cosmetic. These six do actual work:

Header What it prevents
Content-Security-Policy XSS, data injection
Strict-Transport-Security SSL stripping, mixed content
X-Content-Type-Options MIME sniffing attacks
X-Frame-Options Clickjacking
Permissions-Policy Feature abuse (camera, microphone, geolocation)
Referrer-Policy Sensitive URL leakage to third-party scripts

Referrer-Policy is the one that gets skipped most. Setting it to strict-origin-when-cross-origin stops tokens and user IDs embedded in query strings from leaking to analytics or ad platforms via the Referer header.

Setting Headers in Python (FastAPI)

Here is a reusable middleware for FastAPI that applies a production-grade header set:

import secrets
from fastapi import FastAPI, Request, Response
from fastapi.middleware.base import BaseHTTPMiddleware

BASE_CSP = (
    "default-src 'self'; "
    "script-src 'self'; "
    "style-src 'self' 'unsafe-inline'; "
    "img-src 'self' data: https:; "
    "connect-src 'self'; "
    "frame-ancestors 'none'; "
    "base-uri 'self'; "
    "form-action 'self'; "
    "upgrade-insecure-requests"
)

SECURITY_HEADERS = {
    "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY",
    "Referrer-Policy": "strict-origin-when-cross-origin",
    "Permissions-Policy": "geolocation=(), microphone=(), camera=()",
    "Content-Security-Policy": BASE_CSP,
}


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:
        response: Response = await call_next(request)
        for header, value in SECURITY_HEADERS.items():
            response.headers[header] = value
        return response


app = FastAPI()
app.add_middleware(SecurityHeadersMiddleware)
Enter fullscreen mode Exit fullscreen mode

Two things worth noting. First, frame-ancestors 'none' in the CSP supersedes X-Frame-Options in modern browsers, but keep both for legacy support. Second, if you serve HTML with inline scripts — a React build with chunk hashes, for example — you'll need per-request nonces. Generate one with secrets.token_hex(16), inject it into the CSP string and into your template's <script nonce="..."> tags, and you can drop 'unsafe-inline' entirely.

Setting Headers in Go (net/http)

The equivalent middleware for the Go standard library:

package main

import (
    "net/http"
)

var securityHeaders = map[string]string{
    "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
    "X-Content-Type-Options":    "nosniff",
    "X-Frame-Options":           "DENY",
    "Referrer-Policy":           "strict-origin-when-cross-origin",
    "Permissions-Policy":        "geolocation=(), microphone=(), camera=()",
    "Content-Security-Policy":   "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests",
}

func securityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        for header, value := range securityHeaders {
            w.Header().Set(header, value)
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })
    http.ListenAndServe(":8080", securityHeadersMiddleware(mux))
}
Enter fullscreen mode Exit fullscreen mode

This compiles and runs as-is. The CSP here is minimal because a typical Go API serves JSON, not HTML. Adjust script-src and style-src only if your handler returns markup.

The CSP Problem: Why Most Teams Ship It Broken

A Content-Security-Policy that reads default-src * or contains 'unsafe-eval' is security theater — it blocks nothing meaningful. The correct approach is to start restrictive and relax only what you can prove you need.

For a React SPA backed by a Go or Python API, a realistic starting point:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.yourdomain.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://cdn.yourdomain.com;
  connect-src 'self' https://api.yourdomain.com wss://api.yourdomain.com;
  frame-ancestors 'none';
  upgrade-insecure-requests;
  report-uri /csp-violations
Enter fullscreen mode Exit fullscreen mode

The report-uri directive sends a JSON payload to your endpoint every time the policy blocks a load. Deploy in Content-Security-Policy-Report-Only mode first — it reports violations without enforcing — observe for a week, fix the false positives, then flip to enforcement. Skipping this step is why so many CSPs either break the app or get disabled entirely.

The endpoint is trivial: accept POST requests, log the body to your existing log pipeline. The data tells you which CDN domains you forgot and which inline scripts your third-party chat widget injects.

A reference set of ready-to-copy header configurations for common stacks — including the CSP template above — is available in the free security hardening checklists published by AYI NEDJIMI Consultants.

Verifying Headers Don't Regress

Ship the headers, then verify them in CI so they don't disappear in a future refactor. A minimal Python test:

import requests
import sys

REQUIRED_HEADERS = {
    "Strict-Transport-Security",
    "Content-Security-Policy",
    "X-Content-Type-Options",
    "X-Frame-Options",
    "Referrer-Policy",
}

def check_security_headers(url: str) -> None:
    resp = requests.get(url, timeout=10)
    present = {h for h in resp.headers if h in REQUIRED_HEADERS}
    missing = REQUIRED_HEADERS - present
    if missing:
        print(f"FAIL — missing headers: {sorted(missing)}", file=sys.stderr)
        sys.exit(1)
    print(f"OK — all {len(REQUIRED_HEADERS)} required headers present")

if __name__ == "__main__":
    check_security_headers(sys.argv[1])
Enter fullscreen mode Exit fullscreen mode

Run it as a step in your pipeline after deployment:

python check_headers.py https://staging.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

This catches the case where a new nginx config or a proxy layer strips your application headers before they reach the client — a surprisingly common regression.

The Takeaway

Security headers are among the lowest-effort, highest-impact controls available. None of them require architectural changes — a middleware function and a deployment is all it takes. The three you should never skip: HSTS (prevents SSL stripping on first visit), CSP (limits XSS blast radius), and X-Content-Type-Options (stops MIME confusion attacks that turn uploaded files into executable scripts).

Start with report-only CSP mode, observe for a week, enforce. Add a CI check so regressions surface before users do. That is the full playbook.


I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.

Top comments (0)