HTTP Security Headers: The Complete Hardening Guide
Most web servers ship with minimal security headers. Learn which headers protect against XSS, clickjacking, MIME sniffing, and other browser-side attacks — and how to configure them correctly.
Your server can return perfectly valid HTML over a properly configured TLS connection and still leave users exposed to cross-site scripting, clickjacking, data injection, and information leakage. The reason is that browsers make assumptions about how to handle responses, and those assumptions are often wrong from a security perspective. HTTP security headers exist to override those defaults — they are explicit instructions from the server telling the browser how to behave.
The problem is that most web servers and frameworks ship with almost none of these headers configured. The defaults prioritize compatibility, not security. Adding the right headers is one of the highest-impact, lowest-effort hardening steps you can take, yet it is routinely overlooked — and this is exactly why security misconfiguration remains the top finding in external assessments. This guide covers the headers that matter, what they actually do, and how to configure them correctly.
Content-Security-Policy
Content-Security-Policy (CSP) is the single most powerful security header available. It controls which resources the browser is allowed to load and execute on your page — scripts, styles, images, fonts, frames, and more. Without CSP, a browser will execute any script injected into your page, which is exactly what makes cross-site scripting (XSS) attacks so effective.
How It Works
CSP uses directives to define an allowlist for each resource type. The browser enforces these rules and blocks anything that does not match.
A minimal but functional CSP looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'
This tells the browser: only load resources from the same origin, allow data URIs for images, and do not allow this page to be framed at all. Any inline script or style will be blocked because 'unsafe-inline' is not listed.
Strict CSP With Nonces
The allowlist approach has a well-known weakness — if an attacker can host a script on a domain you have allowlisted (such as a CDN that accepts arbitrary uploads), they bypass CSP entirely. The modern recommendation from Google's security team is to use nonce-based CSP:
Content-Security-Policy: script-src 'nonce-a1b2c3d4e5' 'strict-dynamic'; base-uri 'self'; object-src 'none'
The server generates a unique random nonce for each response and adds it to every legitimate <script> tag. The browser only executes scripts with a matching nonce. The 'strict-dynamic' directive allows nonce-authenticated scripts to load additional scripts, which handles most third-party library scenarios.
Common Mistakes
Adding 'unsafe-inline' to make things work. This disables the primary protection CSP provides against XSS. If your application requires inline scripts, use nonces instead.
Overly broad allowlists. A directive like script-src 'self' *.googleapis.com *.cloudflare.com *.jsdelivr.net may look restrictive but includes domains that host user-uploaded or arbitrary content.
Forgetting default-src. Without a default-src directive, any resource type you do not explicitly restrict falls back to allowing everything.
Deploying without testing. Use Content-Security-Policy-Report-Only first. It logs violations without blocking anything, so you can identify legitimate resources your policy would break before enforcement.
Strict-Transport-Security
HSTS tells the browser to never use plain HTTP for a domain. Once the browser receives this header, it internally rewrites all HTTP requests to HTTPS before they leave the machine. No plaintext request is sent, which eliminates SSL stripping attacks.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Configuration Details
max-age is specified in seconds. The minimum recommended value is 31536000 (one year). The example above uses two years. Setting this too low — say, 3600 — means protection expires after one hour. If the user does not revisit within that window, the next request could be over HTTP.
includeSubDomains extends HSTS protection to all subdomains. Without it, api.example.com and cdn.example.com are not covered even if example.com has HSTS. This is required for preloading and strongly recommended in general. Be careful, though — if any subdomain genuinely cannot serve HTTPS, this directive will break it.
preload signals to browser vendors that the domain should be added to the HSTS preload list, which is hardcoded into the browser. This eliminates the first-visit vulnerability where the browser has not yet seen the HSTS header. Preload submission requires max-age of at least one year, includeSubDomains must be present, and the domain must be serving valid HTTPS on all subdomains. Removing a domain from the preload list takes months, so treat this as a permanent commitment.
The First-Visit Problem
Without preloading, HSTS is a trust-on-first-use mechanism. The very first time a user visits your site, if they type http://example.com, that initial request travels over plaintext. An active network attacker can intercept and modify it before the HSTS header is ever delivered. Preloading is the only complete mitigation.
X-Content-Type-Options
X-Content-Type-Options: nosniff
This is a one-value header with a simple job: it prevents the browser from MIME-sniffing a response away from the declared Content-Type. Without it, a browser may decide that a file served as text/plain is actually HTML and render it, or that a file with JavaScript content should be executed even if the server did not intend it.
The attack scenario is straightforward. An attacker uploads a file with a .txt or .jpg extension that actually contains HTML with embedded JavaScript. The server serves it with the correct content type for the extension. The browser ignores the content type, sniffs the actual content, and executes the script. nosniff stops this entirely.
There is no reason not to set this header on every response. It has universal browser support and no compatibility cost.
X-Frame-Options vs frame-ancestors
Clickjacking works by embedding your site in a transparent iframe on an attacker-controlled page. The user thinks they are clicking on the attacker's page but are actually clicking buttons on your site. Preventing framing blocks this class of attack.
The legacy approach is X-Frame-Options:
X-Frame-Options: DENY
Or, to allow framing only by the same origin:
X-Frame-Options: SAMEORIGIN
The modern replacement is the frame-ancestors directive in CSP:
Content-Security-Policy: frame-ancestors 'none'
Or to allow specific origins:
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com
Use frame-ancestors over X-Frame-Options. It supports multiple origins (X-Frame-Options does not), it integrates with your existing CSP, and it is the W3C standard. However, set both headers during transition — some older browsers only respect X-Frame-Options.
Referrer-Policy
When a user navigates from your site to another, the browser sends a Referer header (the original HTTP spec misspelled "referrer" and we are stuck with it) containing the URL they came from. This can leak sensitive information — query parameters with tokens, internal URL paths, and user-specific data.
Referrer-Policy: strict-origin-when-cross-origin
This is the recommended default. It sends only the origin (scheme + host + port) on cross-origin requests, sends the full URL for same-origin requests, and sends nothing when downgrading from HTTPS to HTTP. This balances analytics usefulness with privacy.
For high-sensitivity applications, use no-referrer to suppress the header entirely. For pages that handle tokens or session data in URLs, no-referrer on those specific responses prevents accidental leakage.
Permissions-Policy
Permissions-Policy (formerly Feature-Policy) controls which browser features your site can use — camera, microphone, geolocation, payment, USB, and dozens more. If your site does not need the microphone, explicitly disabling it means that even a successful XSS attack cannot activate it.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
The () syntax means "no origins are allowed." You can grant access to specific origins if needed:
Permissions-Policy: geolocation=(self "https://maps.example.com")
This header is particularly important for sites that embed third-party content. A malicious or compromised ad script cannot access the camera if the top-level page disables it via Permissions-Policy. Similarly, ensuring your cookie security flags are properly configured complements Permissions-Policy by limiting what a compromised script can access.
Putting It All Together
A well-hardened response includes all of these headers working together. Here is a combined example for an Nginx configuration:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; 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 "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
The always keyword in Nginx ensures headers are sent on error responses too — without it, a 404 or 500 page may be served without protection. For copy-paste configuration snippets across Nginx, Apache, and Cloudflare, see our HTTP security headers implementation guide.
What CyberShield Checks
CyberShield's HTTP probe module evaluates all of the headers described in this guide during every scan. It checks for the presence and correctness of Content-Security-Policy, flags policies that include 'unsafe-inline' or 'unsafe-eval', and identifies missing directives like default-src and frame-ancestors. It verifies that Strict-Transport-Security is present with an adequate max-age and notes whether includeSubDomains and preload are configured. It checks for X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy.
Each missing or misconfigured header generates a finding with an appropriate severity level. A missing CSP is not the same risk as a missing Permissions-Policy, and the report reflects that distinction. Findings include specific remediation guidance so you know exactly what to add or change — for step-by-step fixes across all common misconfigurations, see our remediation guide.
Security headers are one of the few areas where a small configuration change delivers disproportionate protection. If your server is not sending them, your TLS configuration and application-level security are doing more work than they should have to.
Continue Reading
Cookie Security: Secure, HttpOnly, and SameSite Flags
Protect session cookies from theft and CSRF attacks by configuring the Secure, HttpOnly, and SameSite flags correctly.
Form Security and CSRF Protection
Secure web forms against cross-site request forgery, method misuse, and insecure action URLs with framework-specific implementation guides and defense-in-depth strategies.
HTTP Security Headers: Complete Implementation Guide
Configure Content-Security-Policy, HSTS, X-Content-Type-Options, and other security headers with copy-paste examples for Nginx, Apache, and Cloudflare.