Why Security Headers Matter
HTTP security headers are instructions your web server sends to the browser, telling it how to behave when handling your site's content. They are one of the most effective and least expensive security measures you can implement. No code changes to your application. No new dependencies. Just a few lines of server configuration.
Yet the majority of websites we audit are missing critical security headers. According to the Scott Helme's annual security headers analysis, less than 15% of the top million websites have a Content-Security-Policy header. This is a fundamental protection that's been available for over a decade.
This guide covers each important security header in depth: what it protects against, how to configure it, what mistakes to avoid, and how to implement it on common platforms. If you want to check your current headers, run your site through securityheaders.com and see where you stand.
Content-Security-Policy (CSP)
CSP is the most powerful and most complex security header. It controls which resources the browser is allowed to load on your page: scripts, styles, images, fonts, frames, and more. A properly configured CSP is your strongest defense against Cross-Site Scripting (XSS) attacks.
How CSP Works
You define a policy that specifies allowed sources for each type of resource. If the browser encounters a resource not allowed by the policy, it blocks it and reports the violation.
A basic CSP header looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Key Directives
| Directive | Controls | Common Values |
|---|---|---|
default-src |
Fallback for all resource types | 'self' |
script-src |
JavaScript sources | 'self', specific CDN domains |
style-src |
CSS sources | 'self' 'unsafe-inline' (see note below) |
img-src |
Image sources | 'self' data: https: |
font-src |
Font sources | 'self', font CDN domains |
connect-src |
XHR, fetch, WebSocket | 'self', API domains |
frame-src |
iframe sources | 'none' or specific domains |
frame-ancestors |
Who can embed your site | 'none' or 'self' |
base-uri |
Allowed base URLs | 'self' |
form-action |
Form submission targets | 'self' |
object-src |
Flash, Java applets | 'none' |
The 'unsafe-inline' Problem
Many sites use 'unsafe-inline' for scripts, which effectively neutralizes CSP's XSS protection. If inline scripts are allowed, an attacker who can inject HTML into your page can also inject a script tag, and CSP won't block it.
The proper alternative is to use nonces or hashes:
- Nonces: Generate a random token for each page load, add it to the CSP header (
script-src 'nonce-abc123'), and add the same nonce to your script tags (<script nonce="abc123">). Only scripts with the matching nonce will execute. - Hashes: Calculate the SHA-256 hash of each inline script and add it to the CSP (
script-src 'sha256-abc...'). Only scripts matching the hash will execute.
For styles, 'unsafe-inline' is less dangerous than for scripts, but you can use the same nonce/hash approach if you want strict protection.
CSP Reporting
CSP supports a report-uri (deprecated) or report-to directive that sends violation reports to a specified endpoint. This is invaluable for monitoring: you can see exactly what your policy is blocking and whether legitimate resources are being caught.
Start with Content-Security-Policy-Report-Only to test your policy without actually blocking anything. Once you are confident the policy doesn't break legitimate functionality, switch to enforcement.
Common CSP Mistakes
- Using
'unsafe-inline'and'unsafe-eval'for scripts. This gives you the complexity of CSP with almost none of the security benefit. - Overly permissive wildcards.
script-src *is effectively no protection at all. - Forgetting
object-src 'none'. Without this, an attacker could use Flash or Java applets to bypass script restrictions. - Missing
base-uri. Without restricting the base URI, an attacker can change the base URL and make all relative URLs point to a malicious server. - Not monitoring reports. A CSP you deploy and never monitor will silently break things or miss attacks.
X-Frame-Options
X-Frame-Options controls whether your site can be embedded in an iframe on another domain. This protects against clickjacking attacks, where an attacker overlays a transparent iframe of your site over a malicious page, tricking users into clicking buttons on your site without knowing it.
Values
DENY- No one can frame your site. Period.SAMEORIGIN- Only pages from the same origin can frame your site.ALLOW-FROM uri- Only the specified origin can frame your site. (Note: this is deprecated and not supported in modern Chrome/Firefox. Use CSP'sframe-ancestorsinstead.)
Recommendation
X-Frame-Options: DENY
Unless you specifically need your site to be embedded in iframes (e.g., for a widget or integration), use DENY. If you do need framing, use CSP's frame-ancestors directive for more granular control.
X-Content-Type-Options
This header prevents MIME type sniffing. Without it, browsers may try to guess the content type of a response by inspecting the content itself, rather than trusting the Content-Type header. An attacker could upload a file with a .jpg extension that actually contains JavaScript, and the browser might execute it.
Configuration
X-Content-Type-Options: nosniff
There is only one valid value. Set it and forget it. There is no reason not to have this header on every response.
Referrer-Policy
The Referrer-Policy header controls how much referrer information the browser sends when navigating from your site to another. The referrer can contain sensitive information: internal URLs, search queries, session tokens in URLs (a bad practice, but it happens).
Values
| Value | Behavior |
|---|---|
no-referrer |
Never send referrer information |
no-referrer-when-downgrade |
Send referrer for HTTPS-to-HTTPS, not HTTPS-to-HTTP (browser default) |
origin |
Send only the origin (domain), not the full URL |
origin-when-cross-origin |
Full URL for same-origin, only origin for cross-origin |
same-origin |
Full referrer for same-origin, nothing for cross-origin |
strict-origin |
Send origin only, and only for HTTPS-to-HTTPS |
strict-origin-when-cross-origin |
Full URL for same-origin, origin for cross-origin HTTPS, nothing for downgrades |
unsafe-url |
Always send the full URL (don't use this) |
Recommendation
Referrer-Policy: strict-origin-when-cross-origin
This is a good balance between privacy and functionality. Same-origin requests get the full referrer (useful for analytics), cross-origin requests get only the origin (not the full path), and no referrer is sent on HTTPS-to-HTTP downgrades.
Permissions-Policy (formerly Feature-Policy)
Permissions-Policy controls which browser features your site can use. This is useful for restricting access to sensitive APIs like camera, microphone, geolocation, and payment requests.
Configuration
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
The empty parentheses () mean the feature is disabled entirely. You can also allow features for specific origins:
Permissions-Policy: geolocation=(self "https://maps.example.com")
Why It Matters
- If your site doesn't use the camera, explicitly disable it. If a third-party script gets injected, it won't be able to access the camera.
interest-cohort=()opts your site out of Google's FLoC (now Topics). This is a privacy consideration.- Third-party iframes inherit permissions from the parent page. If you disable geolocation on your page, an embedded iframe can't access it either.
Strict-Transport-Security (HSTS)
HSTS tells browsers to only connect to your site via HTTPS. After receiving this header, the browser will automatically convert any HTTP requests to HTTPS before sending them, preventing SSL stripping attacks.
For a broader discussion of HTTPS and TLS configuration, see our SSL/TLS certificates guide.
Configuration
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Parameters
max-age=31536000- How long (in seconds) the browser should remember to use HTTPS. 31536000 = one year.includeSubDomains- Apply HSTS to all subdomains as well. Make sure ALL subdomains support HTTPS before enabling this.preload- Allow your domain to be included in the browser's built-in HSTS preload list. This protects even the very first visit.
Deployment Strategy
Don't jump straight to max-age=31536000. If something is wrong with your HTTPS setup, you'll lock users out for a year. Instead:
- Start with
max-age=300(5 minutes). Test everything. - Increase to
max-age=86400(1 day). Monitor for issues. - Increase to
max-age=604800(1 week). - Once confident, go to
max-age=31536000(1 year) and addincludeSubDomainsandpreload.
HSTS Preload
Submitting your domain to hstspreload.org adds it to the preload list bundled with Chrome, Firefox, Safari, and Edge. This means the browser enforces HTTPS even before the first visit. However, removing a domain from the preload list takes months and requires a browser update cycle. Be absolutely sure before submitting.
Additional Useful Headers
Cross-Origin-Opener-Policy (COOP)
Cross-Origin-Opener-Policy: same-origin
Prevents other sites from gaining a reference to your window object through window.open() or window.opener. Protects against Spectre-like side-channel attacks.
Cross-Origin-Resource-Policy (CORP)
Cross-Origin-Resource-Policy: same-origin
Controls whether other origins can include your resources. Setting it to same-origin prevents your images, scripts, etc. from being loaded by other sites.
Cross-Origin-Embedder-Policy (COEP)
Cross-Origin-Embedder-Policy: require-corp
Requires all resources loaded by your page to have explicit CORP headers or be served from the same origin. Together with COOP, enables crossOriginIsolated mode, which is required for high-resolution timers and SharedArrayBuffer.
Testing Your Headers
Several tools can help you verify your security headers:
- securityheaders.com - Scott Helme's grading tool. Enter your URL and get an A-F grade with specific recommendations.
- Mozilla Observatory - Mozilla's comprehensive security assessment tool. Tests headers, TLS configuration, cookies, and more.
- Browser DevTools - Open the Network tab, click on a request, and inspect the response headers directly.
- curl -
curl -I https://example.comshows the response headers from the command line. - CSP Evaluator - Google's tool specifically for analyzing CSP policies and identifying weaknesses.
Implementation Examples
Nginx
Add these to your server block or location block in nginx.conf:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" 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=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
The always keyword ensures headers are sent even for error responses (404, 500, etc.).
Apache
Add these to your .htaccess file or virtual host configuration:
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'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"
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=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Make sure mod_headers is enabled: a2enmod headers
Cloudflare
Cloudflare offers multiple ways to add security headers:
- Transform Rules - In the Cloudflare dashboard, go to Rules > Transform Rules > Modify Response Header. Add each header as a "Set dynamic" or "Set static" rule.
- Workers - For more complex logic, use a Cloudflare Worker to add headers programmatically.
- _headers file - For Cloudflare Pages, create a
_headersfile in your project root with the security headers.
Example _headers file for Cloudflare Pages:
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
Impact on Security Posture
To illustrate the impact of security headers, here is what a website looks like before and after implementation:
| Protection | Without Headers | With Headers |
|---|---|---|
| XSS attacks | Vulnerable | Mitigated by CSP |
| Clickjacking | Vulnerable | Blocked by X-Frame-Options / frame-ancestors |
| MIME sniffing | Vulnerable | Prevented by X-Content-Type-Options |
| SSL stripping | Vulnerable on first visit | Prevented by HSTS |
| Information leakage | Full URLs sent as referrers | Controlled by Referrer-Policy |
| Unauthorized feature access | All features available | Restricted by Permissions-Policy |
| securityheaders.com grade | D or F | A or A+ |
Common Implementation Mistakes
- Setting headers only on the homepage. Security headers need to be on every response, including API responses, error pages, and redirects.
- Duplicating headers. If your application server sets a header and your reverse proxy also sets it, you might end up with duplicate values. This can cause unpredictable behavior. Make sure headers are set in one place only.
- Overly strict CSP that breaks functionality. Test thoroughly in a staging environment. Use report-only mode first. Check all pages, not just the homepage.
- Forgetting about third-party integrations. If you use Google Analytics, a chat widget, or embedded YouTube videos, your CSP needs to allow those sources. Otherwise, they'll break silently.
- Not testing on all browsers. Header parsing can vary slightly between browsers. Test on Chrome, Firefox, Safari, and Edge.
- Enabling HSTS preload prematurely. Once you are in the preload list, removing your domain takes months. Make sure your entire domain (including all subdomains) is fully ready for HTTPS-only.
A Step-by-Step Implementation Plan
If you are starting from scratch, here is a phased approach:
Phase 1: Quick Wins (Day 1)
Add these headers immediately. They are safe and unlikely to break anything:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(), geolocation=()
Phase 2: HSTS (Week 1-2)
Start with a low max-age and gradually increase. Verify all pages and subdomains work correctly over HTTPS before going to full max-age.
Phase 3: CSP (Week 2-4)
- Deploy
Content-Security-Policy-Report-Onlywith a permissive policy and a report-uri. - Analyze reports to understand what resources your site loads and from where.
- Tighten the policy based on the reports. Remove sources you don't need. Switch from wildcards to specific domains.
- Once the report-only policy runs clean for at least a week, switch to enforcement.
- Continue monitoring reports after enforcement.
Phase 4: Advanced Headers (Month 2+)
Add COOP, CORP, and COEP if your application supports them. These require more careful testing as they can affect cross-origin resource loading.
Security Headers and OWASP
The OWASP Secure Headers Project maintains a comprehensive reference for security headers. Many of the headers we have discussed directly address OWASP Top 10 risks, particularly injection attacks (A03:2021) and security misconfiguration (A05:2021). For more on the OWASP Top 10, see our OWASP Top 10 explained article.
Conclusion
Security headers are one of the best return-on-investment security measures available. They require no application code changes, protect against entire classes of attacks, and can be implemented in an afternoon. The key is to approach them methodically: start with the easy wins, add HSTS carefully, and build your CSP through monitoring and iteration.
Your goal should be an A+ on securityheaders.com. It is entirely achievable, and the security benefits are real and measurable.
If you need help implementing security headers or want a comprehensive security assessment of your website, contact our team in Lugano. We help businesses across Switzerland build secure web applications with proper security headers, TLS configuration, and defense-in-depth architectures.
Want to know if your site is secure?
Request a free security audit. In 48 hours you get a complete report.
Request Free Audit