Clickjacking Protection Missing | Blue Frog Docs

Clickjacking Protection Missing

Prevent clickjacking attacks by implementing X-Frame-Options or frame-ancestors CSP directives

Clickjacking Protection Missing

What This Means

Clickjacking (also known as UI redressing) is an attack where a malicious site embeds your website in an invisible iframe and tricks users into clicking on your site's buttons or links while they think they're clicking on something else. Without proper protection (X-Frame-Options header or frame-ancestors CSP directive), attackers can overlay your site with deceptive content and hijack user actions.

How Clickjacking Works

Attack Scenario:

  1. Attacker creates malicious website with invisible iframe
  2. Iframe loads your legitimate website
  3. Attacker overlays enticing buttons/content over your site
  4. User thinks they're clicking attacker's content
  5. Actually clicking invisible buttons on your site
  6. User unknowingly performs actions (delete account, transfer money, change settings)

Example Attack:

<!-- Malicious website: evil.com -->
<html>
  <body>
    <h1>Click here for FREE PRIZE! 🎁</h1>
    <!-- Your legitimate site loaded invisibly -->
    <iframe src="https://yourbank.com/transfer"
            style="position:absolute; top:0; left:0; opacity:0.0001; width:100%; height:100%;">
    </iframe>
    <!-- User clicks "FREE PRIZE" but actually clicks "Confirm Transfer" on your site -->
  </body>
</html>

Impact on Your Business

Security Risks:

  • Account takeover - Users tricked into changing passwords, adding admin users
  • Financial fraud - Unauthorized transactions, fund transfers
  • Data theft - Users manipulated into sharing sensitive information
  • Social engineering - Actions performed without user knowledge or consent
  • Reputation damage - Your site used as attack vector

Common Targets:

  • Banking/financial sites (transfer funds, add payees)
  • Social media (follow accounts, like posts, share content)
  • E-commerce (add items to cart, complete purchases)
  • Admin panels (create accounts, change permissions)
  • Email systems (send emails, forward messages)
  • OAuth flows (grant permissions to malicious apps)

Compliance Impact:

  • Security audit failures
  • PCI DSS requirement violations
  • Regulatory compliance issues
  • Liability for unauthorized actions
  • Customer trust erosion

How to Diagnose

Method 1: Security Headers Check

  1. Visit SecurityHeaders.com
  2. Enter your domain
  3. Review the report
  4. Look for X-Frame-Options or CSP frame-ancestors

What to Look For:

βœ… Protected:

X-Frame-Options: DENY
or
X-Frame-Options: SAMEORIGIN
or
Content-Security-Policy: frame-ancestors 'self'

❌ Vulnerable:

X-Frame-Options: Not set
Content-Security-Policy: No frame-ancestors directive

Method 2: Browser DevTools

  1. Open your website
  2. Open DevTools (F12)
  3. Go to Network tab
  4. Reload page
  5. Click main document
  6. Check Headers β†’ Response Headers

What to Look For:

βœ… PROTECTED:
X-Frame-Options: DENY
or
X-Frame-Options: SAMEORIGIN
or
Content-Security-Policy: frame-ancestors 'self'

❌ VULNERABLE:
(No X-Frame-Options header)
(No frame-ancestors in CSP)

Method 3: Manual Iframe Test

Create a simple test HTML file:

<!-- test-clickjacking.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Clickjacking Test</title>
</head>
<body>
    <h1>Testing if site can be framed:</h1>
    <iframe src="https://your-website.com" width="800" height="600"></iframe>
</body>
</html>

Open the file in browser:

βœ… Protected (Good):

  • Iframe doesn't load
  • Console error: "Refused to display 'https://your-website.com' in a frame because it set 'X-Frame-Options' to 'deny'"
  • Blank frame or error message

❌ Vulnerable (Bad):

  • Website loads in iframe
  • No errors in console
  • Full website visible and functional in frame

Method 4: curl Command

# Check for X-Frame-Options header
curl -I https://your-website.com | grep -i x-frame-options

# Check for CSP frame-ancestors
curl -I https://your-website.com | grep -i content-security-policy

# Expected output (protected):
X-Frame-Options: DENY
# or
Content-Security-Policy: frame-ancestors 'self'

# No output = vulnerable

Method 5: Browser Console Test

// Try to load your site in an iframe programmatically
const iframe = document.createElement('iframe');
iframe.src = 'https://your-website.com';
document.body.appendChild(iframe);

// Check console for errors:
// βœ… Protected: "Refused to display in a frame..."
// ❌ Vulnerable: No error, iframe loads

Method 6: Online Testing Tools

OWASP ZAP:

  1. Install OWASP ZAP
  2. Configure browser proxy
  3. Browse your website
  4. Check Alerts tab
  5. Look for "X-Frame-Options Header Not Set"

Qualys SSL Labs:

  1. Visit SSL Labs
  2. Enter your domain
  3. Review security headers section
  4. Check X-Frame-Options status

General Fixes

Most compatible and simple solution:

DENY (most secure):

X-Frame-Options: DENY
  • Prevents ANY site from framing your pages
  • Recommended for sites that don't need to be embedded
  • Highest security

SAMEORIGIN (balanced):

X-Frame-Options: SAMEORIGIN
  • Only your own site can frame your pages
  • Good for sites with legitimate iframes (admin panels, dashboards)
  • Allows same-domain embedding

ALLOW-FROM (deprecated, don't use):

X-Frame-Options: ALLOW-FROM https://trusted.com
  • ⚠️ NOT supported by Chrome/Safari
  • Use CSP frame-ancestors instead

Fix 2: CSP frame-ancestors Directive (Modern)

Modern, flexible approach:

Deny all framing:

Content-Security-Policy: frame-ancestors 'none'

Allow same-origin only:

Content-Security-Policy: frame-ancestors 'self'

Allow specific domains:

Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com https://partner.com

Combine with other CSP directives:

Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; script-src 'self' https://cdn.example.com

Fix 3: Web Server Configuration

Nginx:

# In nginx.conf or site config
server {
    listen 443 ssl http2;
    server_name example.com;

    # X-Frame-Options
    add_header X-Frame-Options "SAMEORIGIN" always;

    # OR CSP frame-ancestors (more modern)
    add_header Content-Security-Policy "frame-ancestors 'self'" always;

    # OR both for defense in depth
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Content-Security-Policy "frame-ancestors 'self'" always;

    # Rest of your config...
}

Apache (.htaccess):

<IfModule mod_headers.c>
    # X-Frame-Options
    Header always set X-Frame-Options "SAMEORIGIN"

    # OR CSP frame-ancestors
    Header always set Content-Security-Policy "frame-ancestors 'self'"

    # OR both
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Content-Security-Policy "frame-ancestors 'self'"
</IfModule>

Apache (httpd.conf / apache2.conf):

<VirtualHost *:443>
    ServerName example.com

    # X-Frame-Options
    Header always set X-Frame-Options "SAMEORIGIN"

    # OR CSP frame-ancestors
    Header always set Content-Security-Policy "frame-ancestors 'self'"
</VirtualHost>

IIS (web.config):

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <httpProtocol>
            <customHeaders>
                <!-- X-Frame-Options -->
                <add name="X-Frame-Options" value="SAMEORIGIN" />

                <!-- OR CSP frame-ancestors -->
                <add name="Content-Security-Policy" value="frame-ancestors 'self'" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

Fix 4: Application-Level Configuration

Node.js/Express (using Helmet):

const express = require('express');
const helmet = require('helmet');

const app = express();

// Option 1: Use Helmet's frameguard (X-Frame-Options)
app.use(helmet.frameguard({ action: 'sameorigin' }));
// or
app.use(helmet.frameguard({ action: 'deny' }));

// Option 2: Use CSP frame-ancestors
app.use(helmet.contentSecurityPolicy({
  directives: {
    frameAncestors: ["'self'"],
    // other CSP directives...
  }
}));

// Option 3: Both for maximum compatibility
app.use(helmet.frameguard({ action: 'sameorigin' }));
app.use(helmet.contentSecurityPolicy({
  directives: {
    frameAncestors: ["'self'"],
  }
}));

Node.js (manual):

app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
  next();
});

PHP:

<?php
// Add to top of PHP files or in a common header file
header("X-Frame-Options: SAMEORIGIN");
header("Content-Security-Policy: frame-ancestors 'self'");
?>

Python/Flask:

from flask import Flask

app = Flask(__name__)

@app.after_request
def set_security_headers(response):
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
    return response

Python/Django:

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    # ... other middleware
]

# Security settings
X_FRAME_OPTIONS = 'SAMEORIGIN'  # or 'DENY'

# OR use CSP
CSP_FRAME_ANCESTORS = ("'self'",)

Ruby/Rails:

# config/application.rb
config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN'
}

# OR in controller
class ApplicationController < ActionController::Base
  before_action :set_security_headers

  def set_security_headers
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
  end
end

ASP.NET Core:

// Startup.cs
public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
        context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'self'");
        await next();
    });

    // OR use built-in middleware
    app.UseXFrameOptions(XFrameOptionsPolicy.SameOrigin);
}

Java/Spring Boot:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers()
            .frameOptions().sameOrigin()  // X-Frame-Options: SAMEORIGIN
            .contentSecurityPolicy("frame-ancestors 'self'");  // CSP
    }
}

Fix 5: CDN Configuration

Cloudflare:

  1. Log into Cloudflare Dashboard
  2. Select your domain
  3. Go to Rules β†’ Transform Rules β†’ HTTP Response Header Modification
  4. Create new rule:
    • Field: X-Frame-Options
    • Action: Set static
    • Value: SAMEORIGIN

OR use Cloudflare Workers:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const response = await fetch(request)
  const newHeaders = new Headers(response.headers)

  newHeaders.set('X-Frame-Options', 'SAMEORIGIN')
  newHeaders.set('Content-Security-Policy', "frame-ancestors 'self'")

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: newHeaders
  })
}

Netlify (netlify.toml):

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "SAMEORIGIN"
    Content-Security-Policy = "frame-ancestors 'self'"

Vercel (vercel.json):

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Frame-Options",
          "value": "SAMEORIGIN"
        },
        {
          "key": "Content-Security-Policy",
          "value": "frame-ancestors 'self'"
        }
      ]
    }
  ]
}

Fix 6: Allow Specific Embedders

If you need to allow specific trusted sites to embed your content:

CSP frame-ancestors (recommended):

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com https://another-trusted.com

Application logic:

// Node.js example - dynamic X-Frame-Options based on referrer
app.use((req, res, next) => {
  const trustedDomains = [
    'https://trusted-partner.com',
    'https://another-trusted.com'
  ];

  const referrer = req.get('Referer') || '';
  const isTrusted = trustedDomains.some(domain => referrer.startsWith(domain));

  if (isTrusted) {
    res.setHeader('X-Frame-Options', 'ALLOW-FROM ' + referrer);
    // Note: ALLOW-FROM is deprecated, use CSP instead
  } else {
    res.setHeader('X-Frame-Options', 'DENY');
  }

  // Better: Use CSP
  const frameAncestors = isTrusted
    ? `frame-ancestors 'self' ${trustedDomains.join(' ')}`
    : "frame-ancestors 'none'";
  res.setHeader('Content-Security-Policy', frameAncestors);

  next();
});

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify Clickjacking Protection Guide
WordPress WordPress Clickjacking Protection Guide
Wix Wix Clickjacking Protection Guide
Squarespace Squarespace Clickjacking Protection Guide
Webflow Webflow Clickjacking Protection Guide

Verification

After implementing clickjacking protection:

Test 1: HTTP Header Check

# Check X-Frame-Options
curl -I https://your-website.com | grep -i x-frame-options
# Expected: X-Frame-Options: SAMEORIGIN (or DENY)

# Check CSP frame-ancestors
curl -I https://your-website.com | grep -i content-security-policy
# Expected: Content-Security-Policy: frame-ancestors 'self'

Test 2: Browser DevTools

  1. Open your website
  2. DevTools β†’ Network β†’ Reload
  3. Click main document
  4. Headers tab β†’ Response Headers
  5. Verify presence of X-Frame-Options or CSP frame-ancestors

Test 3: Iframe Test

<!-- Create test file: test.html -->
<!DOCTYPE html>
<html>
<body>
    <h1>Clickjacking Protection Test</h1>
    <iframe src="https://your-website.com" width="800" height="600"></iframe>
    <p>If your site loads above, protection is NOT working.</p>
    <p>If you see an error or blank frame, protection is working! βœ…</p>
</body>
</html>

Expected result:

  • Console error: "Refused to display in a frame"
  • Blank iframe
  • Site does not load

Test 4: SecurityHeaders.com

  1. Visit SecurityHeaders.com
  2. Enter your domain
  3. Check for:
    • X-Frame-Options present
    • Or frame-ancestors in CSP
    • Green checkmark βœ…
    • Improved security grade

Test 5: Multiple Pages

Test on various pages:

  • Homepage
  • Login page
  • Admin panel
  • API endpoints
  • Static assets

All should have protection headers.

Common Mistakes

  1. Using ALLOW-FROM - Deprecated, not supported in modern browsers
  2. Only protecting homepage - Apply to all pages and endpoints
  3. Conflicting headers - X-Frame-Options and CSP frame-ancestors conflict
  4. Not testing in production - Development environment differs from production
  5. Forgetting about subdomains - Each subdomain needs protection
  6. Breaking legitimate embeds - Need to allow specific partners
  7. Missing on API endpoints - API responses should also have protection
  8. Not handling errors - Users should know why iframe failed
  9. Cache issues - Old headers cached by browser/CDN
  10. Third-party widgets - May need to allow specific domains

Advanced Topics

Conditional Framing

Allow framing for specific pages only:

# Nginx example
location / {
    add_header X-Frame-Options "DENY" always;
}

location /embed/ {
    add_header X-Frame-Options "SAMEORIGIN" always;
}

location /widget/ {
    add_header Content-Security-Policy "frame-ancestors 'self' https://trusted.com" always;
}

Frame-Busting JavaScript (Deprecated)

⚠️ NOT RECOMMENDED - Can be bypassed, use headers instead:

// Old approach - DO NOT USE
if (top !== self) {
  top.location = self.location;
}

Why it's bad:

  • Can be bypassed with sandbox attribute
  • Doesn't work with CSP
  • JavaScript can be disabled
  • Not reliable security

Defense in Depth

Combine multiple protections:

# Multiple layers of protection
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'; default-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

Monitoring and Alerts

CSP Reporting:

Content-Security-Policy: frame-ancestors 'self'; report-uri /csp-report

Server-side logging:

app.post('/csp-report', (req, res) => {
  console.warn('CSP Violation:', req.body);
  // Log to monitoring system
  res.status(204).end();
});

Choosing Between X-Frame-Options and CSP

Use X-Frame-Options when:

  • Maximum browser compatibility needed
  • Simple DENY or SAMEORIGIN sufficient
  • Not using other CSP directives
  • Legacy browser support required

Use CSP frame-ancestors when:

  • Need to allow multiple specific domains
  • Already using Content-Security-Policy
  • Want more granular control
  • Modern browsers only

Use both when:

  • Maximum compatibility + modern features
  • Defense in depth approach
  • Supporting wide range of browsers
  • No conflicts (ensure values match)

Example - Both:

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

Additional Resources

// SYS.FOOTER