How to score an A+ on a security headers check (and what each header actually does)
A walkthrough of CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy.

You just finished a clean deployment, but the client's security audit returns a sea of red. "Missing CSP," "Insecure HSTS," "Information Leakage via Referrer." You have 20 minutes before the sync call to turn those failing grades into a perfect A+ without bricking your third-party analytics or locking yourself out of your own domain. If you've spent any time in web development, you know that security headers are often treated as a "check the box" exercise. You find a snippet on a forum, paste it into your Nginx config, and hope for the best. But when things break—and they will—you're left wondering which header killed your Stripe checkout or why your fonts suddenly won't load.
This isn't just about getting a badge from a scanner. It's about understanding the specific threats each header mitigates and the specific ways they can break your site if misconfigured. In this guide, we're going to walk through the essential security headers, explain exactly what they do, and provide you with a production-ready configuration that meets 2024 standards.
Beyond the Badge: Why Security Header Scores Actually Matter
There is a tendency in the industry to view security headers as "security theatre." Critics argue that if your application code is vulnerable to SQL injection, a Content-Security-Policy won't save you. They are partially right—headers are not a silver bullet. However, they represent your primary defense against a massive class of client-side attacks, including Cross-Site Scripting (XSS), Clickjacking, and Protocol Downgrades.
The Anatomy of a Modern Browser Attack
Modern web security is a layer-cake. Your first layer is your application code (sanitizing inputs, using parameterized queries). Your second layer is the browser environment itself. Security headers are instructions you send to the browser, telling it to restrict its own capabilities for the sake of the user's safety. For example, without the X-Content-Type-Options: nosniff header, a browser might try to "guess" the type of a file based on its contents. If an attacker manages to upload a malicious script disguised as an image, an older or less-strict browser might execute it as JavaScript. The header stops that behavior cold.
How Scanners Calculate Your Grade
Scanners like GlitchReplay's Security Headers Scorecard or Scott Helme's SecurityHeaders.com use a weighted scoring system based on current OWASP recommendations. To get an A+, you typically need a comprehensive Content-Security-Policy (CSP), a long-duration Strict-Transport-Security (HSTS) header with preload, and strict policies for Referrer and Permissions. A "Default" server response usually contains none of these, leaving you with an F. A "Hardened" response ensures that the browser only trusts what you explicitly allow.
# Default (Unsafe) Headers
HTTP/2 200 OK
Content-Type: text/html
Content-Length: 1245
Server: nginx/1.25.0
# Hardened (A+) Headers
HTTP/2 200 OK
Content-Type: text/html
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' https://scripts.glitchreplay.com; object-src 'none';
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()Content-Security-Policy (CSP): The Swiss Army Knife of Defense
If you only implement one header today, make it the Content-Security-Policy. CSP is a powerful tool that allows you to define which sources of content (scripts, styles, images, frames) are trusted. It is the single most effective defense against XSS.
Why default-src 'self' is the Gold Standard
The most common mistake developers make with CSP is being too permissive. They start with default-src * and then try to block specific things. This is the wrong way around. A secure CSP starts with default-src 'self', which tells the browser: "By default, don't trust anything unless it comes from my own origin." This immediately blocks all inline scripts, all third-party domains, and all inline styles.
Handling Third-Party Scripts
Of course, a modern web app isn't just your code. You have Google Analytics, Stripe for payments, and maybe Intercom for support. Each of these requires an exception in your CSP. The "old" way was to allow-list entire domains (e.g., https://www.google-analytics.com). The "modern" way is to use a Strict CSP pattern, which utilizes nonces (number used once) to authorize specific script tags regardless of their source.
For a standard Next.js or React application, a safe starter CSP looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://js.stripe.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https://*.stripe.com;
connect-src 'self' https://api.glitchreplay.com https://*.google-analytics.com;
frame-src https://js.stripe.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;Note: If you use a framework like Next.js with Server Components, you can often remove 'unsafe-inline' by generating a nonce in your middleware and passing it to your script tags.
Transitioning from report-only to Enforcement
If you apply a strict CSP to a production site today, you will likely break something. This is why the Content-Security-Policy-Report-Only header exists. It allows you to monitor what would have been blocked without actually blocking it. You should run in report-only mode for at least a week, using a tool like the GlitchReplay CSP Violation Decoder to parse the reports and adjust your policy before you flip the switch to full enforcement.
Strict-Transport-Security (HSTS): The Point of No Return
The HSTS header forces the browser to communicate with your server exclusively over HTTPS. Even if a user types http://yoursite.com or clicks a legacy link, the browser will transparently upgrade the request before it ever leaves the user's machine. This prevents "SSL Stripping" attacks where an attacker on a public Wi-Fi network downgrades your connection to intercept traffic.
The "Bricking" Risk
The danger of HSTS is that it is cached by the browser. If you set a max-age of two years and your SSL certificate expires or you accidentally break your HTTPS configuration, your users are locked out. There is no "undo" button once that header is in the user's browser cache. This is why you must use a tiered rollout plan.
The 3-Step Rollout Plan
- The 5-Minute Test:
max-age=300. If something breaks, it only lasts five minutes. - The One-Week Validation:
max-age=604800; includeSubDomains. Ensure your subdomains (likeblog.yoursite.comorapi.yoursite.com) also have valid SSL certificates. - The Full Commitment:
max-age=63072000; includeSubDomains; preload. This sets the duration to two years and tells browser vendors to bake your domain into their source code.
To be eligible for the HSTS Preload List, your max-age must be at least 1 year (31,536,000 seconds), though 2 years is now the recommended standard for an A+ grade. Once preloaded, your site is HTTPS-only from the very first visit, eliminating the "Trust On First Use" (TOFU) vulnerability.
Preventing Framing and Sniffing: X-Frame-Options and X-Content-Type-Options
While CSP can handle framing and sniffing protection, many scanners still look for these older, dedicated headers. They are simple to implement and carry very little risk of breaking your site.
X-Frame-Options: Clickjacking Protection
Clickjacking involves an attacker loading your site inside an invisible <iframe> on their own domain. They place a fake button over your "Delete Account" button, tricking the user into clicking it. X-Frame-Options: DENY tells the browser that your site should never be framed. If you actually need to frame your own site (e.g., for a CMS preview), use SAMEORIGIN.
The "Magic" of nosniff
Browsers sometimes try to be too helpful. If a server sends a file with a .txt extension but it looks like a script, the browser might execute it anyway. This is called MIME-sniffing. By setting X-Content-Type-Options: nosniff, you tell the browser: "Trust the Content-Type header I sent you. Don't try to guess." This prevents a specific class of XSS where attackers upload text files containing malicious code.
If you are using Cloudflare, you can implement these via Transform Rules without touching your origin code. If you are using Next.js, you can add them to your next.config.js:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
]
},
}Privacy and Hardware Control: Referrer-Policy and Permissions-Policy
Security isn't just about preventing attacks; it's also about preserving user privacy and limiting the attack surface of the browser itself.
Referrer-Policy: Stopping Data Leaks
By default, when a user clicks a link on your site to an external domain, the browser sends the full URL of your page in the Referer header. If your URLs contain sensitive data (like /reset-password?token=123), that data is now in the external site's logs. The best practice is strict-origin-when-cross-origin. This sends the full URL to your own pages, but only sends the domain (the origin) to third parties, and sends nothing if the connection is downgraded to HTTP.
Permissions-Policy: Disabling the Hardware
The Permissions-Policy (formerly known as Feature-Policy) allows you to explicitly disable browser features that your site doesn't use. Does your B2B SaaS dashboard need access to the user's camera? Their microphone? Their accelerometer? If not, disable them. This ensures that even if an attacker finds an XSS vulnerability, they can't use it to turn on the user's webcam.
A strict policy for a standard web app might look like this:
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), payment=()The empty parentheses () tell the browser that no origin is allowed to use these features. If you use Stripe and need to support Apple Pay or Google Pay, you would change it to payment=(self "https://js.stripe.com").
The "One-Click" Audit: Validating your config with GlitchReplay
Configuring headers is one thing; keeping them correct as your app evolves is another. Every time you add a new marketing tool or move a subdomain, you risk breaking your CSP or dropping a required header. This is where continuous monitoring becomes essential.
Running a Live Scan
You can use the GlitchReplay Security Headers Scorecard to get an immediate, prioritized list of what's missing. Unlike generic scanners, we focus on the impact. We won't just tell you that you're missing a header; we'll explain how that specific omission leaves you vulnerable to current exploits.
Interpreting the Checklist
When you run a scan, look for the "A+ Requirements" checklist. Common hurdles include:
- HSTS duration: Is your
max-ageat least 31,536,000? - CSP Object-Src: Did you remember to set
object-src 'none'to prevent Flash-based attacks? - Insecure Schemes: Does your CSP accidentally allow
http:sources?
Continuous Monitoring
A one-time check isn't enough. In a modern CI/CD environment, headers should be part of your automated testing suite. If a developer accidentally removes the upgrade-insecure-requests directive from the CSP, your build should fail. Security headers are a living part of your infrastructure, not a set-it-and-forget-it configuration.
Conclusion: A Production-Ready Checklist
To reach an A+ grade and, more importantly, to actually secure your users, use the following checklist for your next deployment. Here is the "Copy-Paste" header block for a standard Nginx configuration:
# Nginx Security Headers Block
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" 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=(), interest-cohort=()" always;Remember that the CSP provided above includes 'unsafe-inline' and 'unsafe-eval'. While these are often necessary for legacy apps or certain framework behaviors, your ultimate goal should be to remove them by using nonces and refactoring inline scripts. This is the difference between a "good" security posture and a truly "hardened" one.
Stop guessing your security posture. Drop your URL into the GlitchReplay Security Headers Scorecard right now. It takes ten seconds, and it will give you a prioritized fix list that you can hand off to your team before that sync call even starts.
GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.