EponwebPractical guides to web development and technology
Web Security

6 Security Headers You Must Enable to Prevent XSS and Clickjacking

Forget complex WAFs for a moment; these six HTTP headers are the cheapest, most effective upgrade you can make to your stack today.

Lucas Ferreira
Lucas FerreiraLead Frontend Engineer7 min read
Editorial image illustrating 6 Security Headers You Must Enable to Prevent XSS and Clickjacking

Two weeks ago, a peer of mine at a fintech startup called me in a panic. They had passed their penetration test, but a script kiddie managed to load a deceptive "login" overlay on top of their customer dashboard using an iframe. The attacker didn't exploit a zero-day; they simply exploited a missing header.

While we often obsess over complex logic vulnerabilities or race conditions, the fundamental gatekeepers of web security sit quietly in the HTTP response headers. If the browser doesn't receive instructions to the contrary, it will assume a permissive stance—allowing mixed content, framing, and MIME type sniffing.

Here are the six headers you need to enable right now to lock down your application against injection and UI-based attacks.

Why Content-Security-Policy (CSP) is Your Strongest Ally

If you only implement one header from this list, make it this one. Content-Security-Policy (CSP) acts as a whitelist for resources, effectively mitigating Cross-Site Scripting (XSS) by telling the browser exactly which sources of executable scripts are legitimate.

In 2026, we have moved past the days of 'unsafe-inline' for most applications. If your CSP configuration currently allows inline scripts, you are merely relying on obfuscation. The modern standard involves using nonces or hashes.

Consider the following configuration for a React or Vue application:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-2726c7f26c' https://analytics.trusted.com; object-src 'none'; base-uri 'self';

This header tells the browser: "Only load scripts from the current origin, and only if they contain the specific nonce 2726c7f26c generated for this request." Even if an attacker manages to inject <script>alert(1)</script> into the DOM, the browser will refuse to execute it because the injected tag lacks the correct cryptographic nonce.

The Trade-off: CSP is strict. It breaks lazy-loaded third-party widgets and requires careful management if you are using a CDN for static assets. However, the security payoff—neutralizing the vast majority of XSS vectors—is worth the configuration headache.

Browser Support: Supported in all modern browsers (Chrome 25+, Firefox 23+, Safari 7+). Legacy Internet Explorer requires the X-Content-Security-Policy prefix, which we can safely ignore in 2026.

Stopping Clickjacking with X-Frame-Options and frame-ancestors

Clickjacking attacks trick a user into clicking something different than what the user perceives, potentially revealing confidential information or taking control of their computer while another site is displayed in a transparent iframe. While the frame-ancestors directive in CSP is the modern solution, the legacy X-Frame-Options header is still required for older browser versions.

I typically recommend implementing both for defense in depth.

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';

Setting X-Frame-Options to DENY prevents any domain from framing your content. If you need to allow your own subdomains (e.g., a dashboard at app.example.com framing a help widget at support.example.com), use SAMEORIGIN.

However, relying solely on X-Frame-Options is no longer sufficient. The CSP frame-ancestors directive offers granular control. For instance, if you need to embed a specific page in a trusted partner's site, you can whitelist them explicitly without opening the door to everyone else.

Common Mistake: Using ALLOW-FROM uri. This directive was never standardized and is ignored by Chrome and Safari. Stick to DENY, SAMEORIGIN, or the CSP frame-ancestors directive.

Browser Support: X-Frame-Options has near-universal support. frame-ancestors is supported in all modern browsers.

Enforcing Encryption via Strict-Transport-Security (HSTS)

Once your application is available over HTTPS, you must ensure that clients never attempt to connect over an unencrypted HTTP connection. Without HSTS, a man-in-the-middle attacker can strip the TLS encryption during the initial connection (SSL stripping), forcing the user to interact with a plain HTTP version of the site.

The header looks deceptively simple:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

This configuration instructs the browser to remember for two years (63,072,000 seconds) that this domain should only be accessed via HTTPS. The includeSubDomains directive ensures that blog.example.com is just as secure as www.example.com.

Critical Warning: Be absolutely certain your HTTPS configuration is rock-solid before enabling this header. If you misconfigure your SSL/TLS certificate and users have HSTS cached, they will be unable to access your site until the max-age expires. The preload directive submits your domain to the HSTS Preload list, hardcoding these requirements into the browser source code.

If you are currently in the process of migrating to HTTPS, start with a low max-age (e.g., 300 seconds) to test the waters before committing to a long duration.

Browser Support: Supported in IE 11+ (Edge), Chrome, Firefox, and Safari. Not supported in HTTP sites (obviously) or HTTP-only connections.

Photographic detail related to 6 Security Headers You Must Enable to Prevent XSS and Clickjacking

Curbing MIME Sniffing with X-Content-Type-Options

Browsers, trying to be helpful, will sometimes "sniff" the content of a response to determine the file type if the Content-Type header is missing or ambiguous. This behavior can lead to security vulnerabilities, such as executable scripts being run when they were uploaded as images.

A classic exploit involved uploading a file with a .jpg extension that actually contained valid JavaScript. If the server served it with a generic content type (like text/plain or application/octet-stream), older versions of Internet Explorer would execute it as HTML.

To disable this sniffing, use the following:

X-Content-Type-Options: nosniff

This forces the browser to adhere strictly to the declared Content-Type. If the header says image/jpeg but the response contains a script tag, the browser will refuse to execute it.

While this behavior has been largely standardized in recent Chrome and Firefox updates, adding this header is a zero-cost security measure that protects users on older browsers and enforces strict content handling.

Browser Support: IE 8+, Chrome, Edge, Firefox, Safari.

Controlling Information Leakage with Referrer-Policy

Developers often underestimate how much data leaks via the Referer header (note the misspelling, which is historical). Every time a user navigates from Page A to Page B, the URL of Page A is sent to Page B. If Page A contains sensitive data in the query string—like a session token or a reset password hash—that data is leaked to third-party domains.

The Referrer-Policy header controls how much information is included in the Referer header during navigation.

I recommend the strict-origin-when-cross-origin policy for most applications:

Referrer-Policy: strict-origin-when-cross-origin

This configuration works as follows:

  1. Same-origin: Send the full URL as referrer (safe, since you stay on your site).
  2. Cross-origin: Send only the origin (scheme, host, and port), stripping the path and query parameters.

This prevents sensitive URLs from leaking to external analytics providers or advertisers while still allowing you to see internal traffic patterns.

Alternative: For maximum privacy, use no-referrer, though this breaks analytics and affiliate tracking completely.

Browser Support: Supported in all major browsers since 2020.

Isolating the Process with COOP and COEP

These are the newest entries to the security header roster, and they address advanced threats like Spectre and side-channel attacks. Cross-Origin-Opener-Policy (COOP) and Cross-Origin-Embedder-Policy (COEP) work together to isolate your browsing context.

By setting COOP to same-origin, you ensure that cross-origin documents cannot open your site in a popup window. This prevents malicious sites from messing with your window.opener reference.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Setting COEP to require-corp ensures that cross-origin resources loaded into your document (like images or scripts) must explicitly opt-in via CORS headers with Cross-Origin-Resource-Policy: cross-origin.

The "Why": This isolation enables powerful features like SharedArrayBuffer (necessary for high-performance webAssembly) but, more importantly, it prevents other tabs from reading your memory.

The Trade-off: COEP is strict. It will block third-party scripts or images that do not send the correct CORS headers. Implementing this often requires a significant audit of your asset pipeline.

Browser Support: Chrome 85+, Edge 85+, Firefox 79+ (partial/full support varies).

Moving Beyond Basic Configuration

Headers are not a "set it and forget it" solution. As your architecture evolves—perhaps shifting to a Jamstack approach or implementing JWT vs. Session Cookies: Why Sessions Are Still Safer for Standard Web Apps—your security policy must evolve too.

Furthermore, headers must work in concert with other security mechanisms. For example, a strict CSP is less effective if your session management is flawed. We discussed previously how SameSite Cookies Explained: The First Line of Defense Against CSRF are essential for request forgery protection. These layers are interconnected; a hole in one undermines the strength of the others.

The final step after deploying these headers is continuous monitoring. Automated auditing tools like securityheaders.com or Lighthouse in CI pipelines can catch regressions. Don't wait for an audit to discover that your frame-ancestors directive was accidentally removed during a deployment. Treat security headers as part of your application code—reviewed, tested, and versioned.

Read next