PenTesting Company
Nighttime satellite view of the United States, showing city lights from coast to coast
← Blog

Using X-Frame-Options to Avoid Clickjacking in Your Web Application

8 min read

X-Frame-Options is a response header that tells the browser whether other sites may load your pages in a frame. Setting it reduces the risk of clickjacking, where an attacker hides your page under bait content to capture a user's clicks. DENY blocks all framing, SAMEORIGIN allows framing only by your own origin, and the older ALLOW-FROM value is obsolete and ignored by modern browsers. The Content Security Policy frame-ancestors directive is the modern standard for the same control, so sending both is generally the better practice.

Short answer

X-Frame-Options config generator

Pick your setup to get a framing header snippet and a curl check. Everything runs in your browser and nothing is sent anywhere. Output is illustrative, so review it against your own setup before applying it.

Server stack
Framing policy
CSP frame-ancestors
1. Config
# server or location block (illustrative)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;

# Then: sudo nginx -t && sudo systemctl reload nginx
2. Check (run against a site you own, replacing example.com)
curl -sI https://example.com | grep -i -E "x-frame-options|content-security-policy"

# Expected:
x-frame-options: SAMEORIGIN
content-security-policy: frame-ancestors 'self'

If you already send a Content-Security-Policy header, add the frame-ancestors directive to that policy instead of sending a second header.

Clickjacking tricks a user into clicking something different from what they think they are clicking. An attacker loads your page in a hidden or transparent frame, positions it over bait content, and captures the click. It is an old technique, but plenty of applications still leave the door open.

How it works

Web pages are built in layers. Using CSS, an attacker can stack an invisible frame of your site on top of their own page and line up its buttons with theirs. When the user clicks, the action lands on your application in the context of their session.

X-Frame-Options vs CSP frame-ancestors

Use DENY or SAMEORIGIN as the legacy fallback, and put the real policy in frame-ancestors. Browsers that support CSP2 use frame-ancestors when both are set, so keep the two aligned.

X-Frame-Options vs CSP frame-ancestors: effect, browser support, and when to use each directive
DirectiveEffectBrowser supportWhen to use
X-Frame-Options: DENYBlocks all framing, including from your own origin.Legacy response header still read by browsers that implement X-Frame-Options.Nothing should embed the page, and you want a fallback beside CSP.
X-Frame-Options: SAMEORIGINAllows framing only by pages on the same origin (scheme, host, and port).Legacy response header still read by browsers that implement X-Frame-Options.Your own origin needs to embed the page, as a fallback beside CSP.
X-Frame-Options: ALLOW-FROMWas meant to name one framing origin. Modern browsers ignore it.Obsolete and inconsistently supported. Do not rely on it.Do not use. Put named origins in CSP frame-ancestors instead.
frame-ancestors 'none'Blocks all framing. CSP equivalent of DENY.Browsers with CSP Level 2. Those browsers use this instead of X-Frame-Options when both are set.Modern default when no site should embed the page.
frame-ancestors 'self'Allows framing only by the same origin. CSP equivalent of SAMEORIGIN.Browsers with CSP Level 2.Modern default when your own origin may embed the page.
frame-ancestors with specific originsAllows framing by the origins you list, such as https://app.example.com.Browsers with CSP Level 2. X-Frame-Options has no supported equivalent for named origins.A known origin you control needs to embed the page. Keep the allowlist tight.

Controlling who can frame your pages

The X-Frame-Options response header tells the browser whether your pages may be framed:

  • DENY blocks all framing.
  • SAMEORIGIN allows framing only by pages on your own origin.

The older ALLOW-FROM value is deprecated and inconsistently supported. For modern browsers, prefer the Content Security Policy frame-ancestors directive, which is more flexible and lets you name specific origins:

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

Set both for broad coverage. Clickjacking is a common finding in a web security test, and it is usually a quick fix once identified.

Server snippets

These are illustrative. Review them against your own server before applying them. Nginx needs always, and Apache needs Header always set, so error pages get the same headers. If you already send a Content-Security-Policy header, add frame-ancestors to that policy instead of sending a second header. The generator above can adapt the values. The same add_header ... always; pattern appears when you hide the Nginx Server header.

# Nginx, server or location block (illustrative)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
# Apache, VirtualHost or .htaccess, needs mod_headers (illustrative)
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
// Express with helmet (illustrative)
import helmet from "helmet";

app.use(
  helmet({
    xFrameOptions: { action: "sameorigin" },
    contentSecurityPolicy: {
      directives: {
        "frame-ancestors": ["'self'"],
      },
    },
  }),
);

Framing headers are a separate control from CORS, which tells the browser which origins may read responses. These headers tell it which origins may embed the page.

Verify checklist

  • Against a site you own, run curl -sI https://your-site | grep -iE 'x-frame|frame-ancestors' and confirm the values you intended.
  • On your own machine only, open a local HTML page that frames a URL you control and confirm the browser blocks or allows it as expected. Do not frame third-party sites.
  • Repeat the curl check on an error page and on a redirect, not only on a 200 response.
  • Confirm X-Frame-Options and frame-ancestors agree. Browsers that support CSP2 use frame-ancestors when both are set, so a DENY header will not override a looser frame-ancestors allowlist in those browsers.

Missing framing headers are a common finding in a web application security test. A penetration test is not a compliance certification. If you would like us to review framing headers along with the rest of the application, we can include that in a scoped web application test against a scope we agree together. Contact us to get started.

Frequently asked questions

Is X-Frame-Options deprecated?

X-Frame-Options is not removed. DENY and SAMEORIGIN still work in browsers that implement the header, and they remain a useful legacy fallback. The modern standard for the same control is Content-Security-Policy frame-ancestors. ALLOW-FROM is the obsolete value: modern browsers ignore it. Sending DENY or SAMEORIGIN together with a matching frame-ancestors directive is generally the better practice.

X-Frame-Options vs frame-ancestors: which wins if both are set?

Browsers that support CSP2 use frame-ancestors and do not apply X-Frame-Options when both are present. Older browsers that do not apply frame-ancestors still read X-Frame-Options. Align the two so they do not disagree. For example, DENY with frame-ancestors 'none', or SAMEORIGIN with frame-ancestors 'self'.

How do I allow framing only by my own subdomain?

X-Frame-Options SAMEORIGIN is origin-exact. A page on https://app.example.com is not same-origin with https://example.com or https://www.example.com, so SAMEORIGIN will not allow those hosts to frame it. ALLOW-FROM is obsolete and ignored. Use CSP frame-ancestors with an explicit allowlist of the origins you intend, such as frame-ancestors 'self' https://example.com https://app.example.com. Some CSP implementations also accept a wildcard host such as https://*.example.com. Keep that list tight, and keep X-Frame-Options as DENY or SAMEORIGIN only if it matches the framing you actually want in older browsers.

Does X-Frame-Options stop all clickjacking?

No. The header reduces the risk of framing-based clickjacking by telling the browser who may load your pages in a frame. It does not make an application secure, and it does not stop every way a user can be tricked into taking an action. Send CSP frame-ancestors as well, set the headers on every HTML response including error pages, and keep the rest of your application controls in place.

How do I test my own site for clickjacking safely?

Only test a site you own or are authorized to test. Check the headers with curl -sI https://your-site | grep -iE 'x-frame|frame-ancestors' on a success response, an error page, and a redirect. Then, on your own machine, open a local HTML page that iframes a URL you control and confirm the browser blocks or allows framing as you expect. Do not frame or test third-party sites. If X-Frame-Options and frame-ancestors disagree, browsers that support CSP2 follow frame-ancestors.

Nighttime satellite view of the United States, showing city lights from coast to coast

Have a system you want tested?

Tell us what you are working with and we will follow up with next steps.