Vulnerability
Next.js

Clickjacking in Next.js Applications

Next.js does not set X-Frame-Options or CSP frame-ancestors by default, leaving applications vulnerable to clickjacking. An attacker embeds your Next.js app in a transparent iframe on their malicious page, tricking users into clicking buttons they cannot see — triggering actions like deleting accounts, changing settings, or transferring funds.

Scan Your Next.js App

How Clickjacking Manifests in Next.js

Without X-Frame-Options or frame-ancestors headers, any website can embed your Next.js application in an iframe. The attacker creates a page with your app loaded in a transparent iframe positioned over clickable elements on their page. Next.js applications with authenticated state-changing actions (delete account, change email, transfer funds) are prime targets. The attacker positions the invisible iframe so that when users click what they think is a button on the attacker's page, they actually click the dangerous action in your app. Single-page applications built with Next.js are especially vulnerable because the entire application is framed at once, giving the attacker access to any page and any action the user can perform. Server Actions and form submissions within the iframe execute with the user's authenticated session. The App Router and Pages Router behave identically regarding clickjacking — neither adds frame protection headers by default. Vercel, Netlify, and other hosting platforms also do not add these headers automatically.

Real-World Impact

An attacker targeted a Next.js banking application by creating a page that said "Click here to claim your prize." The prize button was positioned exactly over the "Confirm Transfer" button in an invisible iframe loading the bank's transfer page. Users who clicked the button unknowingly confirmed a wire transfer to the attacker's account. A Next.js admin dashboard was embedded in an iframe on a phishing page designed to look like a system notification. When admins clicked "Dismiss notification," they actually clicked "Delete all user data" in the admin dashboard. The lack of X-Frame-Options made this attack possible.

Step-by-Step Fix

1

Add X-Frame-Options and frame-ancestors in next.config.js

Configure both headers for maximum browser compatibility.

// next.config.js
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'Content-Security-Policy',
            value: "frame-ancestors 'none'",
          },
        ],
      },
    ];
  },
};
module.exports = nextConfig;

// Use SAMEORIGIN instead of DENY if your app
// legitimately iframes itself (e.g., preview features):
// { key: 'X-Frame-Options', value: 'SAMEORIGIN' }
2

Add frame protection via Middleware

If you already use Middleware for other security headers, add frame protection there.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set(
    'Content-Security-Policy',
    "frame-ancestors 'none'"
  );
  return response;
}

export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
};
3

Allow framing from specific trusted domains

If your app must be embedded by specific partner sites, use frame-ancestors with an allowlist instead of DENY.

// next.config.js - when embedding is required
const nextConfig = {
  async headers() {
    return [
      {
        // Pages that should NEVER be framed
        source: '/(dashboard|settings|admin)(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
        ],
      },
      {
        // Embeddable widget pages - only from trusted domains
        source: '/embed/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "frame-ancestors 'self' https://trusted-partner.com",
          },
          // Note: X-Frame-Options does not support allowlists,
          // so we omit it for embedded routes
        ],
      },
    ];
  },
};
module.exports = nextConfig;

Prevention Best Practices

1. Set X-Frame-Options: DENY in next.config.js headers to block all framing. 2. Use CSP frame-ancestors: 'none' as the modern replacement for X-Frame-Options. 3. If your app must be embedded in specific domains, use frame-ancestors with an allowlist. 4. Add both headers for backward compatibility — older browsers only support X-Frame-Options. 5. Verify the headers appear on all routes including API endpoints that return HTML.

How to Test

1. Create a simple HTML file with <iframe src="https://your-app.com"></iframe> and open it locally. If your app loads in the iframe, it is vulnerable. 2. Check response headers with curl -I https://your-app.com/ for X-Frame-Options or frame-ancestors. 3. Verify protection on authenticated pages, not just the homepage. 4. Test both the Pages Router and App Router if your app uses both. 5. Use Vibe App Scanner to automatically detect missing clickjacking protection in your Next.js application.

Frequently Asked Questions

What is the difference between X-Frame-Options and frame-ancestors?

X-Frame-Options is the older header that supports DENY and SAMEORIGIN values. CSP frame-ancestors is the modern replacement that additionally supports allowlisting specific domains (e.g., frame-ancestors https://trusted.com). Use both for maximum browser compatibility — older browsers ignore frame-ancestors, and newer browsers prefer it over X-Frame-Options.

Does clickjacking work if my Next.js app uses CSRF tokens?

Yes. Clickjacking bypasses CSRF protection because the user is making a legitimate, authenticated request from their own browser — they are just tricked into clicking. CSRF tokens validate that the request came from your site's form, and in a clickjacking attack, the request does come from your site (loaded in the iframe). Only frame-restriction headers prevent clickjacking.

Should I use DENY or SAMEORIGIN for X-Frame-Options?

Use DENY unless your application has a legitimate need to frame itself (e.g., a page builder with a live preview iframe). SAMEORIGIN allows your own domain to frame the page, which is needed for some features but increases the attack surface if XSS is present — an attacker with XSS could create an iframe on your domain to clickjack users.

Is Your Next.js App Vulnerable to Clickjacking?

VAS automatically scans for clickjacking vulnerabilities in Next.js applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Next.js stack.