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.
Content-Security-Policy
CSP controls which resources the browser is allowed to load. Without it, injected scripts run freely, making cross-site scripting trivial to exploit. For a conceptual deep dive into how each header defends against specific attack classes, see our complete HTTP security headers guide.
Basic Policy
A restrictive baseline that blocks inline scripts and limits all resources to the same origin:
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'; object-src 'none'
This policy breaks sites that rely on inline <script> or <style> tags. That is intentional. Inline code is the primary XSS attack vector.
Strict Policy With Nonces
Allowlist-based CSP has a known bypass: if any allowlisted domain serves user-controlled content, an attacker can host a payload there. Nonce-based CSP eliminates this problem.
Content-Security-Policy: script-src 'nonce-RANDOM_BASE64' 'strict-dynamic'; style-src 'nonce-RANDOM_BASE64'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'
Your server generates a cryptographically random nonce per response and injects it into every legitimate <script> and <style> tag. The browser only executes elements whose nonce attribute matches the header value. The 'strict-dynamic' directive lets nonce-authenticated scripts load additional scripts, which handles most third-party library patterns.
Replace RANDOM_BASE64 with an actual per-request random value. Never hardcode or reuse nonces.
Report-URI and Reporting
Deploy CSP in report-only mode first to find breakage before enforcement:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-reports; report-to csp-endpoint
The report-uri directive (legacy, still widely supported) and report-to directive (newer Reporting API) send JSON violation reports to your endpoint. Run report-only for at least a week in production before switching to enforced mode. Review violations to distinguish legitimate resources you need to allowlist from actual attack attempts.
Strict-Transport-Security
HSTS instructs the browser to rewrite all HTTP requests to HTTPS internally, before any network traffic is sent.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Recommended values: Set max-age to at least 31536000 (one year). Two years (63072000) is standard for production. Anything below six months is too short -- if a user does not revisit within that window, protection expires.
includeSubDomains extends the policy to all subdomains. Required for preload submission and strongly recommended in general. Verify that every subdomain can serve valid HTTPS before enabling this.
Preload considerations: Adding preload signals browser vendors to hardcode your domain into the HSTS preload list. This eliminates the first-visit vulnerability where the browser has never seen the header. However, preload list removal takes months. Do not add preload unless you are certain HTTPS will remain available on the root domain and all subdomains indefinitely. Submit your domain at hstspreload.org after the header is live.
X-Content-Type-Options
X-Content-Type-Options: nosniff
Prevents the browser from MIME-sniffing a response. Without this, a file served as text/plain that contains HTML or JavaScript may be rendered or executed. Set this on every response. There are no compatibility downsides.
X-Frame-Options vs frame-ancestors
Both prevent clickjacking by controlling whether your page can be embedded in an iframe.
Legacy (X-Frame-Options):
X-Frame-Options: DENY
Modern (CSP frame-ancestors):
Content-Security-Policy: frame-ancestors 'none'
To allow framing by specific origins:
Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com
Use frame-ancestors as your primary control. It supports multiple origins, which X-Frame-Options does not. Set both headers during transition periods because some older browsers only respect X-Frame-Options.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
This is the recommended default. It sends the full URL for same-origin navigations, only the origin (scheme + host) for cross-origin requests, and nothing when downgrading from HTTPS to HTTP. This prevents query parameters with tokens, internal paths, and user-specific data from leaking to third-party sites.
For pages that handle sensitive tokens in URLs, set no-referrer on those specific responses. Pair this with proper cookie security flags to ensure session tokens are never exposed through either referrer headers or insecure cookie transport.
Permissions-Policy
Permissions-Policy restricts which browser APIs your site can access. Disabling unused APIs limits the damage an XSS attack can do.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()
The () value means no origin is permitted. To grant access to your own origin only:
Permissions-Policy: geolocation=(self)
Review the full list of policy-controlled features and disable everything your application does not use.
Deprecated Headers to Remove
X-XSS-Protection -- This header controlled a browser heuristic XSS filter that was removed from all modern browsers. The filter itself had vulnerabilities that could be exploited to create XSS in otherwise safe pages. Remove this header entirely. Do not set it to 1; mode=block. CSP replaces its function.
X-Powered-By -- Leaks server technology (e.g., Express, PHP/8.2, ASP.NET). This gives attackers free reconnaissance. Remove it in your server config rather than overriding it with a fake value. Header misconfigurations are just one category of findings — see our common security misconfigurations guide for a broader remediation checklist.
Server Configuration
Nginx
# Security headers — add to server or location block
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'; object-src 'none'" 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;
# Remove deprecated headers
proxy_hide_header X-Powered-By;
more_clear_headers 'X-XSS-Protection';
The always keyword ensures headers are sent on error responses (404, 500) as well. Without it, Nginx only adds headers to successful responses.
Apache
# Security headers — add to VirtualHost or .htaccess
Header always set 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'; object-src 'none'"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
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 Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
# Remove deprecated headers
Header always unset X-Powered-By
Header always unset X-XSS-Protection
Requires mod_headers to be enabled. Run a2enmod headers and restart Apache if these directives are not recognized.
Cloudflare
In the Cloudflare dashboard, navigate to Rules > Transform Rules > Managed Response Headers. Cloudflare provides a one-click preset for common security headers. For full control, create a custom rule:
- Go to Rules > Transform Rules > HTTP Response Header Modification.
- Set the rule to match all requests (
URI Path contains /or a custom filter). - Add each header as a Set static action:
| Header Name | Value |
|---|---|
| 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'; object-src 'none' |
| 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 | camera=(), microphone=(), geolocation=(), payment=(), usb=() |
- Add Remove actions for
X-Powered-ByandX-XSS-Protection.
Alternatively, use a Cloudflare Worker for dynamic CSP with nonces:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const response = await fetch(request);
const nonce = crypto.randomUUID().replace(/-/g, '');
const headers = new Headers(response.headers);
headers.set('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'`);
headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Frame-Options', 'DENY');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=()');
headers.delete('X-Powered-By');
headers.delete('X-XSS-Protection');
return new Response(response.body, { status: response.status, headers });
}
Verification
After deploying headers, verify them with a curl check:
curl -I https://yourdomain.com
Inspect each header in the response. Confirm that deprecated headers are absent and all security headers are present with correct values. Run a CyberShield scan to get a detailed assessment of header coverage and policy strength.
Continue Reading
WAF Detection and Fingerprinting: How CyberShield Identifies Web Application Firewalls
Understanding what web application firewall protects a target is essential context for any security assessment. Learn how CyberShield passively fingerprints 15+ WAF vendors through header analysis, error patterns, and behavioral signatures.
Web Vulnerability Assessment: What Passive Analysis Reveals Without Firing a Single Exploit
Passive web analysis uncovers OWASP-relevant vulnerabilities -- information leaks, form weaknesses, exposed files, and redirect flaws -- without touching a single exploit.
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.