Table of Contents
HTTP security headers are one of the most effective and underutilized defenses available to web application operators. These response headers instruct browsers to enable built-in security features that protect users against cross-site scripting (XSS), clickjacking, protocol downgrade attacks, and a range of other client-side threats.
Despite their effectiveness, adoption remains inconsistent. An analysis of the top 1 million websites found that only 12.8% implement a meaningful Content-Security-Policy, and just 25.3% use HSTS. This guide covers the essential headers every site should implement, with practical configuration examples for the most common server environments.
Why Security Headers Matter
Security headers operate at the browser level, providing a final layer of defense even when application code contains vulnerabilities. A well-configured Content-Security-Policy can prevent the exploitation of an XSS vulnerability that might otherwise lead to account takeover. HSTS ensures that users connect securely even if an attacker attempts to intercept the initial connection.
These headers are free to implement, require no software installation, and can be deployed at the web server, reverse proxy, or CDN level. There is no reason for any production website to lack basic security header coverage.
Content-Security-Policy (CSP)
CSP is the most powerful — and most complex — security header available. It defines a whitelist of approved content sources for each resource type (scripts, styles, images, fonts, etc.), preventing the browser from loading unauthorized content.
Recommended Starting Policy
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';
This baseline policy restricts all resources to the same origin by default, allows inline styles (often necessary for legacy applications), permits images from HTTPS sources, and prevents the site from being framed by other domains.
Apache Configuration
# In .htaccess or httpd.conf
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
Nginx Configuration
# In server block or location block
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
Cloudflare Configuration
In Cloudflare, security headers can be configured through Transform Rules (HTTP Response Header Modification) in the dashboard under Rules, or via Cloudflare Workers for more complex logic:
// Cloudflare Worker example
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';");
return newResponse;
}
Pro tip: Start with Content-Security-Policy-Report-Only instead of Content-Security-Policy to monitor violations without blocking content. Add a report-uri or report-to directive to collect violation reports, then tighten the policy iteratively until no legitimate resources are blocked.
Strict-Transport-Security (HSTS)
HSTS instructs browsers to only connect to your site via HTTPS, preventing protocol downgrade attacks and cookie hijacking. Once a browser receives an HSTS header, it will automatically convert all HTTP requests to HTTPS for the specified duration — even if the user types http:// explicitly.
Recommended Configuration
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000— Enforce HTTPS for one year (in seconds)includeSubDomains— Apply the policy to all subdomainspreload— Signal eligibility for the HSTS preload list, which hardcodes HTTPS enforcement into browsers
Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
includeSubDomains, verify that all subdomains support HTTPS. If any subdomain is HTTP-only, users will be unable to access it. Before submitting to the preload list at hstspreload.org, understand that removal is difficult and slow — only preload when you are fully committed to HTTPS across your entire domain.
X-Frame-Options
X-Frame-Options prevents your site from being embedded in iframes on other domains, defending against clickjacking attacks where an attacker overlays invisible frames to trick users into clicking on your site's controls.
X-Frame-Options: DENY
Use DENY to prevent all framing, or SAMEORIGIN to allow framing only from pages on the same origin. Note that CSP's frame-ancestors directive is the modern replacement and offers more granular control, but X-Frame-Options should still be set for compatibility with older browsers.
Apache
Header always set X-Frame-Options "DENY"
Nginx
add_header X-Frame-Options "DENY" always;
X-Content-Type-Options
This header prevents browsers from MIME-sniffing a response away from the declared content type. Without it, browsers may interpret a file as a different type than what the server specified — for example, treating a text file as JavaScript — which can enable XSS attacks through uploaded files.
X-Content-Type-Options: nosniff
This is a simple, one-value header with no configuration options. There is no reason not to set it on every response.
Apache
Header always set X-Content-Type-Options "nosniff"
Nginx
add_header X-Content-Type-Options "nosniff" always;
Referrer-Policy
Referrer-Policy controls how much referrer information is sent when users navigate away from your site. By default, browsers may include the full URL — including query parameters that might contain sensitive data like session tokens, search queries, or tracking identifiers.
Recommended Configuration
Referrer-Policy: strict-origin-when-cross-origin
This policy sends the full referrer for same-origin requests (useful for analytics), sends only the origin (domain) for cross-origin HTTPS-to-HTTPS requests, and sends nothing for HTTPS-to-HTTP downgrades. This strikes a good balance between privacy and functionality.
Other useful values:
no-referrer— Never send referrer information (maximum privacy, but breaks some analytics and integrations)same-origin— Only send referrer for same-origin requestsorigin— Always send only the origin, never the full URL
Apache
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Permissions-Policy
Permissions-Policy (formerly Feature-Policy) controls which browser features and APIs can be used on your site. This prevents malicious scripts — whether from XSS or compromised third-party resources — from accessing sensitive capabilities like the camera, microphone, or geolocation.
Recommended Configuration
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
The empty parentheses () disable the feature entirely. To allow a feature for your own origin only, use (self). To allow specific third-party origins, list them explicitly: (self "https://trusted-partner.com").
Apache
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
Nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
Common Mistakes
Even when organizations implement security headers, common configuration errors can reduce or eliminate their effectiveness:
- Using
unsafe-inlineandunsafe-evalin CSP script-src: These directives effectively disable CSP's XSS protection. If your application requires inline scripts, use nonce-based ('nonce-randomValue') or hash-based whitelisting instead. - Setting HSTS max-age too low: A
max-ageof a few minutes or hours provides minimal protection. Use at leastmax-age=31536000(one year) for production sites. - Missing the
alwayskeyword in Nginx: Withoutalways, Nginx only adds headers to successful (2xx) responses, leaving error pages unprotected. - Duplicate headers: If headers are set at both the application level and the web server level, some servers will send duplicate headers with potentially conflicting values. Verify your final response headers.
- Overly permissive CSP with wildcard sources: A CSP like
script-src *provides essentially no protection. Wildcards should be used sparingly and only for resource types with lower risk (e.g.,img-src). - Forgetting to test with
Report-Onlyfirst: Deploying a restrictive CSP without testing can break site functionality. Always deploy in report-only mode first and monitor for violations.
Testing Tools
Verify your security header implementation using these tools:
securityheaders.com— Free online scanner that grades your security header implementation from A+ to F, with specific recommendations.- Mozilla Observatory (
observatory.mozilla.org) — Comprehensive web security scanner that evaluates headers alongside other security configurations. - Chrome DevTools: Open the Network tab, click on any request to your site, and inspect the Response Headers section to verify headers are present and correctly formatted.
- curl: Quick command-line verification:
curl -I https://yourdomain.com - CSP Evaluator (
csp-evaluator.withgoogle.com) — Google's tool for analyzing CSP policies and identifying weaknesses. - Report URI (
report-uri.com) — Service for collecting and analyzing CSP violation reports and other security header reports.
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
Security headers are not a substitute for secure application code, but they provide a critical additional layer of defense that can prevent the exploitation of vulnerabilities that slip through code review and testing. Implementing these headers is one of the highest-impact, lowest-effort security improvements any web application can make.