James Galley Software engineer - SaaS, AI automation & business operations

X-Frame-Options HTTP header

How to set the X-Frame-Options header to prevent clickjacking, with examples for Apache, PHP, Yii 2, Node.js and Remix - and what the "refused to display in a frame" browser error means.

The X-Frame-Options HTTP header is one of the HTTP security headers available to enhance the security of a website or web application. Although it is made obsolete by the frame-ancestors directive available in the Content-Security-Policy header, browsers still support X-Frame-Options and it can be used to manage or prevent the framing of your site's content in other websites. It is one piece of a wider approach to web application security.

X-Frame-Options is a response header generated by a server in response to an HTTP request, and it can be used to prevent rendering of the page in <frame>, <iframe>, <embed>, <object> HTML elements.

Like all the HTTP security headers, this header must be set in an HTTP response by your web server or content delivery mechanism. That means using a server-side scripting language like PHP or Node.js or a web server framework like Apache or Nginx. When working with AWS CloudFront, perhaps for static sites deployed to S3 or apps running through API Gateway, it's possible to use Lambda@Edge or CloudFront functions to add headers into the viewer response.

Sites can use the X-Frame-Options header to ensure that their content is not embedded and rendered on other sites, helping to prevent click-jacking attacks and malicious rendering of your content across the web.

Directives

X-Frame-Options has only two possible values which are simple string values:

X-Frame-Options: DENY

DENY indicates to the browser that the page cannot be displayed in a frame in any location, even framed with the same site and domain.

X-Frame-Options: SAMEORIGIN

SAMEORIGIN indicates to the browser that the page can be framed within the same host origin. An example use-case for this directive is when using Yii 2's debugger for local development, as this module loads a framed window originating from the same host, and will be blocked if the DENY directive is used.

There is a deprecated directive of ALLOW-FROM=url which previously offered the ability to list permitted domains, however this is now unsupported and you should use the frame-ancestors directive available in the Content-Security-Policy header for this purpose.

Fixing "refused to display in a frame" errors

If you're here because of a browser console error like this one:

Refused to display 'https://example.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.

then the header is doing its job: the page you're trying to embed is telling the browser not to allow it. If you control the framed site, set the header to SAMEORIGIN (if the framing happens on the same domain) or replace it with a Content-Security-Policy frame-ancestors directive listing the domains allowed to frame it, using the platform examples below. If you don't control the framed site, there is no way to override the error from the embedding side - the block is enforced by the browser at the request of the site being framed, and that's precisely the protection the header exists to provide.

How to set X-Frame-Options header in Apache

The h5bp/server-configs-apache library is a great reference for this (this version from v6.0.0, 5th December 2022):

<IfModule mod_headers.c>
    Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

How to set X-Frame-Options in PHP

<?php
header('X-Frame-Options: DENY');
?>

For Yii2, headers can be added site-wide by modifying the response object in the app config (thanks to this SO post):

'components' => [
    'response' => [
        'on beforeSend' => function($event) {
            $event->sender->headers->add('X-Frame-Options', 'DENY');
        },
    ],

Note that Apache/.htaccess settings of a header will overwrite any value set in PHP.

How to set X-Frame-Options in Node.js / Remix.run

In the Remix JavaScript framework response headers are set in the handleRequest function of the server entry script, typically app/entry.server.jsx.

import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
  request,
  responseStatusCode,
  responseHeaders,
  remixContext
) {
  let markup = renderToString(
    <RemixServer context={remixContext} url={request.url} />
  );
  responseHeaders.set("Content-Type", "text/html");

  // Set X-Frame-Options Header
  responseHeaders.set("X-Frame-Options", "SAMEORIGIN");

  return new Response("<!DOCTYPE html>" + markup, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
}

See more information on Remix entry scripts here: https://remix.run/docs/en/v1/guides/migrating-react-router-app#creating-server-and-browser-entrypoints