VibeSecurity

Fix

Fix: X-Frame-Options missing (clickjacking protection)

Without framing protection, any website can load your app inside an invisible iframe and trick a logged-in user into clicking your buttons. Two response headers prevent it.

By the VibeSecurity team2 min read

What this means

The finding means your pages do not tell browsers who may embed them. In a clickjacking attack, a hostile page places your site in a transparent iframe over a decoy, so the victim's click lands on a real button in your app, such as delete, transfer or grant access. The modern control is the CSP frame-ancestors directive. X-Frame-Options is the older header and is still worth sending for older browsers.

Why it happens

  • Frameworks and most hosts do not send either header by default.
  • Generated apps rarely include security headers unless the prompt asks for them.
  • The header was set in a meta tag. Browsers ignore X-Frame-Options and frame-ancestors in meta tags. They must be real response headers.
  • Someone used X-Frame-Options: ALLOW-FROM, which modern browsers do not support.

How to fix it

  1. 1Decide whether any site needs to frame your pages. For most apps the answer is no.
  2. 2Add both headers to all responses in your framework or hosting config.
  3. 3If a partner must embed you, list their origin in frame-ancestors, for example frame-ancestors 'self' https://partner.example, and leave out X-Frame-Options on those routes only, since it cannot express an allow-list.
  4. 4If you already send a Content-Security-Policy header, add frame-ancestors to that policy instead of sending a second CSP header.
  5. 5Deploy and check the pages that matter most: login, settings, payments and admin.
next.config.js, with vercel.json and Netlify equivalents
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
          { key: 'X-Frame-Options', value: 'DENY' },
        ],
      },
    ];
  },
};

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

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

How to confirm the fix

Check the headers with curl, then try to frame your own site. Save the HTML below as a local file, replace the URL with your own, and open it in a browser. After the fix the frame stays empty and the console shows a refusal such as 'Refused to display ... in a frame'.

curl and a local framing test for your own site
curl -s -D - -o /dev/null https://www.yourapp.com/login | grep -iE 'x-frame-options|content-security-policy'

<!doctype html>
<title>Framing test</title>
<iframe src="https://www.yourapp.com/login" width="800" height="600"></iframe>

Frequently asked questions

Do I need both headers?

frame-ancestors is the current standard and takes priority in browsers that support it. X-Frame-Options covers older browsers. Sending both is cheap.

Will this break my embedded widgets, such as a payment form?

No. These headers control who may frame your pages. They do not affect frames your pages load from others. That is controlled by frame-src.

Is SameSite on my cookies enough?

It helps, because a framed cross-site page may load without the session cookie, but OWASP still recommends the framing headers as the primary defence.

Sources

  1. 1.MDN: X-Frame-Options
  2. 2.MDN: CSP frame-ancestors
  3. 3.OWASP Clickjacking Defense Cheat Sheet
  4. 4.RFC 7034: HTTP Header Field X-Frame-Options
  5. 5.Vercel: vercel.json headers