Security headers

HTTP response headers that harden your app against common browser-based attacks

What these are

Beyond the headers covered in their own articles - HSTS, CSP, CORS - there are several smaller security-focused response headers worth sending. None of them are dramatic on their own, but they close off classes of attack cheaply, and a well-configured server sends all of them.

Django's SecurityMiddlewarehandles most of these. In a standard project it's already in your MIDDLEWARE list - you just need to enable the relevant settings.

X-Content-Type-Options

X-Content-Type-Options: nosniff

Browsers have a behaviour called MIME sniffing: if a response doesn't have a Content-Type header, or the browser suspects the declared type is wrong, it will look at the content itself and guess what it is. A file served as text/plain that looks like HTML might be rendered as HTML. A file that looks like JavaScript might be executed as JavaScript.

This matters because an attacker who can upload a file to your server - an image, say - could craft it to look like JavaScript when sniffed. If the browser executes it, that's XSS. nosniff tells the browser to trust the declared Content-Type and never guess. If the type is wrong, the resource is blocked rather than sniffed.

In Django: SECURE_CONTENT_TYPE_NOSNIFF = True (this is already the default in Django 3.0+).

Referrer-Policy

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

When a user clicks a link from your site to another site, browsers include a Referer header (note: historically misspelled in the spec) telling the destination where the user came from. By default this is the full URL - including path and query string.

That full URL can leak sensitive information. If your URL looks like /reset-password?token=abc123 or /accounts/profile?user_id=9182, any external site the user navigates to from that page receives that URL in the Referer header. Third-party scripts on your own page (analytics, ads) also see it.

strict-origin-when-cross-origin is the sensible default: it sends the full URL for same-origin requests (fine - same site, no leakage) and only the origin (e.g. https://example.com, no path) for cross-origin requests.

In Django: set SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin". This is also the default in modern Django.

Permissions-Policy

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

Allows you to restrict which browser features your page - and any iframes it contains - can use. An empty value (()) disables the feature entirely. You can also allow it only for your own origin, or for specific third-party origins.

The threat this addresses: third-party scripts embedded on your page (analytics, chat widgets, ad networks) can request access to browser features you never intended to use. A permissions policy prevents that at the browser level, regardless of what those scripts try to do.

Common features to lock down: camera, microphone, geolocation, payment, usb. Only allow what your application actually needs. Django doesn't have a built-in setting for this - you'd set it via middleware or a custom response header.

X-XSS-Protection (legacy)

X-XSS-Protection: 0

Older browsers had a built-in XSS filter that tried to detect and block reflected XSS attacks. X-XSS-Protection: 1; mode=block told the browser to enable it.

This header is now deprecated. The XSS filters in older browsers were found to introduce their own vulnerabilities - the filter could be tricked into blocking legitimate content or, worse, into creating new XSS vectors. Modern browsers have removed the filter entirely.

The correct approach now is to explicitly send X-XSS-Protection: 0to disable it in any old browser that still has it, and rely on a proper Content Security Policy instead. It's worth knowing this header exists and why you should set it to zero rather than one.

Django settings summary

# settings.py

SECURE_CONTENT_TYPE_NOSNIFF = True       # X-Content-Type-Options: nosniff
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"

# No built-in for Permissions-Policy - add via middleware:
# response["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"

# X-Frame-Options is handled by XFrameOptionsMiddleware (see Clickjacking)
# HSTS is handled by SECURE_HSTS_* settings (see HSTS)
# CSP requires a separate package or middleware (see Content Security Policy)

Checking your headers

securityheaders.com scans a URL and grades your response headers, flagging anything missing or misconfigured. Worth running against any production site. You can also check with curl:

curl -I https://example.com
Read nextContent Security PolicyCSP is the most powerful security header of all - a full allowlist of what your page is permitted to load and execute.