Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 3 – A02:2025 Security Misconfiguration

Clickjacking and framing controls

What clickjacking is, which pages must refuse frames, and how frame-ancestors, X-Frame-Options, iframe sandbox and SameSite work together, tested in code.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain clickjacking and decide which pages of an app must refuse to be framed
  • Predict whether a browser renders a framed page from its frame-ancestors and X-Frame-Options headers and its ancestors
  • Configure embedding on purpose for named origins only, and sandbox the third-party frames you embed
  • Explain why frame-busting scripts and SameSite cookies are not enough on their own

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain clickjacking and decide which pages of an app must refuse to be framed
  • Predict whether a browser renders a framed page from its frame-ancestors and X-Frame-Options headers and its ancestors
  • Configure embedding on purpose for named origins only, and sandbox the third-party frames you embed
  • Explain why frame-busting scripts and SameSite cookies are not enough on their own

Before you start

On this page

A page that sends no framing header can be shown inside any other site’s page. In this lesson’s simulator 6 of 16 setups let another site do exactly that, and 5 of those 6 carry a rule that was meant to stop it, such as X-Frame-Options: ALLOW-FROM, a frame-ancestors rule in a meta element or a report-only policy. clickjacking: Showing a page of one site inside a frame on another site, where the user does not notice it, lined up under something harmless, so that a real click or keystroke acts on the framed page. Also called UI redress (CWE-1021). turns such a frame into a real click on a button the user never saw.

You will decide which pages must refuse frames, predict the browser’s verdict for sixteen setups, let named partners embed a page on purpose, sandbox the frames you embed yourself, and see why frame-busting scripts and SameSite cookies cannot replace the headers.

Clickjacking turns a real click into an action the user never saw

Clickjacking, also called UI redress, needs only one thing from your site: that another site may show your page in a frame. MDN describes the rest. The other site’s page places your page where the user does not notice it, lined up with something harmless of its own, so that the user’s genuine click acts on your page. OWASP files the weakness under A06:2025 Insecure Design as CWE-1021, improper restriction of rendered UI layers or frames, and the fix is a header your server sends. Step through one such visit, then the same visit with the fix:

Interactive · step by step A real click on a page the user never saw Step through a page on another site that frames your account page, then the same visit once your page refuses to be framed.
A real click on a page the user never saw, step 6 of 6. Showing Page on other.example, Frame of your page (refused, nothing shown), The user's click, Close account button, Browser, bank.example server (frame-ancestors 'none'). In focus: Frame of your page.frames itrender or refusePage on other.exampleshows a harmless buttonFrame of your pagerefused, nothing shownThe user's clickaimed at what they seeClose account buttonone clickBrowserreads your headersbank.example serverframe-ancestors 'none'

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Another site's page. The user opens a page on other.example. It shows a button with a harmless label.
  2. Your page in a frame. The page shows your account page in a frame the user does not notice, lined up so that your Close account button sits where the decoy's button appears. Nothing tells the user that your page is there.
  3. A real click. The user clicks what they see. The click lands on your button inside the frame, because the frame is on top.
  4. A signed-in request. If the browser sent the user's cookies with the frame, your server receives a genuine click from a signed-in user and closes the account. It had no way to tell.
  5. The defence is in your headers. Now your page answers with frame-ancestors 'none' in its Content-Security-Policy header. The browser reads it before it shows anything in the frame.
  6. The frame stays empty. The browser refuses to render your page inside other.example's frame. The click lands on the decoy's own button, and your server receives nothing.

Nothing in the request told your server that the click came through someone else’s page. That is why the defence belongs in the response, before the browser shows anything: your page tells the browser who may frame it, and everyone else gets an empty frame.

Which pages need it? Every page where one click, a drag or a few keystrokes do something that matters: paying, deleting, changing an email address or a password, granting an app access in an OAuth consent screen, signing in. In practice that is most of a signed-in app, so the sane default is that no page may be framed, and embedding is an exception you configure for the few pages that need it. ASVS 5.0.0 requirement 3.4.6 says the same: frame-ancestors on every response, blocking embedding by default.

Key idea

Clickjacking needs only that another site may frame your page, so refuse framing by default and allow it only on the pages that must be embedded.

Checkpoint

Check: What clickjacking needs, and which pages to protect

  1. Question 1 of 2 Which pages of a signed-in shop must refuse to be framed by other sites? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • The form that changes the account's email address
    • The OAuth consent screen that grants an app access
    • The page that confirms a payment with one click

    Every page where a click, a drag or a few keystrokes do something that matters must refuse frames. In a signed-in app that is most pages, which is why refusing by default is the sane setting.

  2. Question 2 of 2 Where does the defence against clickjacking belong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In the response headers of your own page, which tell the browser who may frame it

    Nothing in the request shows that the click came through another site's frame. The response decides: a frame-ancestors header makes the browser leave the frame empty.

frame-ancestors decides, X-Frame-Options is the fallback

Two headers control framing, and the browser reads them by rules that are easy to get wrong:

  • Content-Security-Policy: frame-ancestors lists who may frame the page: 'none', 'self', origins such as https://partner.example, wildcard subdomains or a scheme. CSP Level 3 checks every frame ancestor: A document that contains a frame, directly or further up: the parent, its parent and so on to the top-level page. frame-ancestors and X-Frame-Options SAMEORIGIN check every one of them, not only the parent., the parent and each page above it to the top, and any one that does not match makes the browser refuse. The directive works only in an enforced header: it is ignored in a meta element, and it does not fall back to default-src.
  • X-Frame-Options knows two values, DENY and SAMEORIGIN, and the HTML standard checks SAMEORIGIN against every ancestor too. A browser reads it only when no enforced frame-ancestors exists: CSP Level 3 says the directive overrides the header. A lone unknown value, ALLOW-FROM included, counts as no header at all, and two different values make the browser refuse when one of them is DENY or SAMEORIGIN.

This program applies both rules to sixteen setups of one bank page. Each case names the headers and the chain of ancestors that frames it:

Sixteen framing setups and the browser's verdict JavaScript · verdicts/framing_policy.mjs
// mayRender(page, headers, ancestors) answers what a browser does when the page arrives in a frame: render it or
// refuse it. It follows two specifications: CSP Level 3 for frame-ancestors (every ancestor is checked; the directive
// counts only in an enforced header, never in a meta element or a report-only policy) and the HTML standard's check
// of X-Frame-Options, which applies only when no enforced frame-ancestors exists. Source matching covers the forms
// this lesson uses: 'none', 'self', https://host, https://*.host and the bare scheme https:.

const originOf = (url) => url.match(/^[a-z]+:\/\/[^/]+/)[0];

function matches(source, ancestor, self) {
  if (source === "'self'") return ancestor === self;
  if (/^[a-z]+:$/.test(source)) return ancestor.startsWith(`${source}//`);
  const [scheme, host] = source.split('://');
  const [aScheme, aHost] = ancestor.split('://');
  if (scheme !== aScheme) return false;
  if (host.startsWith('*.')) return aHost.endsWith(host.slice(1));
  return aHost === host;
}

function mayRender(page, headers, ancestors) {
  if (ancestors.length === 0) return ['render', 'not in a frame'];
  const self = originOf(page);
  const fa = /frame-ancestors([^;]*)/.exec(headers.csp ?? '');
  if (fa) {
    const sources = fa[1].trim().split(/\s+/);
    for (const a of ancestors) {
      if (sources[0] === "'none'" || !sources.some((s) => matches(s, a, self))) return ['refuse', `frame-ancestors does not allow ${a}`];
    }
    return ['render', headers.xfo ? 'frame-ancestors allows every ancestor; X-Frame-Options ignored' : 'frame-ancestors allows every ancestor'];
  }
  if (headers.xfo) {
    const values = new Set(headers.xfo.split(',').map((v) => v.trim().toLowerCase()));
    const known = ['deny', 'sameorigin', 'allowall'].some((v) => values.has(v));
    if (values.size > 1) return known ? ['refuse', 'X-Frame-Options has conflicting values'] : ['render', 'X-Frame-Options has no valid value'];
    if (values.has('deny')) return ['refuse', 'X-Frame-Options DENY'];
    if (values.has('sameorigin')) {
      const other = ancestors.find((a) => a !== self);
      return other ? ['refuse', `SAMEORIGIN, and ${other} is an ancestor`] : ['render', 'SAMEORIGIN, every ancestor is the same origin'];
    }
    return ['render', `X-Frame-Options "${headers.xfo}" is ignored`];
  }
  const ignored = headers.cspMeta
    ? 'frame-ancestors in a meta element is ignored'
    : headers.cspReportOnly
      ? 'report-only, so the frame is only reported'
      : headers.csp
        ? 'no frame-ancestors, and default-src does not cover it'
        : 'no framing control';
  return ['render', ignored];
}

const BANK = 'https://bank.example/account';
const SELF = 'https://bank.example';
const OTHER = 'https://other.example';
const PARTNER = 'https://partner.example';

const cases = [
  ['no headers', {}, [OTHER]],
  ['XFO DENY', { xfo: 'DENY' }, [OTHER]],
  ['XFO DENY, own page frames it', { xfo: 'DENY' }, [SELF]],
  ['XFO SAMEORIGIN, own page', { xfo: 'SAMEORIGIN' }, [SELF]],
  ['XFO SAMEORIGIN, other site on top', { xfo: 'SAMEORIGIN' }, [SELF, OTHER]],
  ['XFO ALLOW-FROM partner', { xfo: 'ALLOW-FROM https://partner.example' }, [OTHER]],
  ['XFO SAMEORIGIN and DENY', { xfo: 'SAMEORIGIN, DENY' }, [SELF]],
  ["CSP 'none', own page", { csp: "frame-ancestors 'none'" }, [SELF]],
  ["CSP 'self' partner, partner", { csp: "frame-ancestors 'self' https://partner.example" }, [PARTNER]],
  ['CSP partner, with XFO DENY', { csp: "frame-ancestors 'self' https://partner.example", xfo: 'DENY' }, [PARTNER]],
  ['CSP partner, other on top', { csp: 'frame-ancestors https://partner.example' }, [PARTNER, OTHER]],
  ["CSP 'none' in a meta element", { cspMeta: "frame-ancestors 'none'" }, [OTHER]],
  ["CSP 'none', report-only", { cspReportOnly: "frame-ancestors 'none'" }, [OTHER]],
  ["CSP default-src 'none' only", { csp: "default-src 'none'" }, [OTHER]],
  ['CSP *.bank.example, bank.example', { csp: 'frame-ancestors https://*.bank.example' }, [SELF]],
  ['CSP https:, any https site', { csp: 'frame-ancestors https:' }, [OTHER]],
];

console.log(`The page: ${BANK}. Ancestors: the parent first, then up to the top.`);
const open = [];
cases.forEach(([name, headers, ancestors], i) => {
  const [verdict, why] = mayRender(BANK, headers, ancestors);
  if (verdict === 'render' && ancestors.some((a) => a === OTHER)) open.push(i + 1);
  console.log(`${String(i + 1).padStart(2)}. ${name.padEnd(35)}${verdict.padEnd(8)}${why}`);
});
console.log(`Rendered inside a page of other.example: cases ${open.join(', ')}`);

Output

The page: https://bank.example/account. Ancestors: the parent first, then up to the top.
 1. no headers                         render  no framing control
 2. XFO DENY                           refuse  X-Frame-Options DENY
 3. XFO DENY, own page frames it       refuse  X-Frame-Options DENY
 4. XFO SAMEORIGIN, own page           render  SAMEORIGIN, every ancestor is the same origin
 5. XFO SAMEORIGIN, other site on top  refuse  SAMEORIGIN, and https://other.example is an ancestor
 6. XFO ALLOW-FROM partner             render  X-Frame-Options "ALLOW-FROM https://partner.example" is ignored
 7. XFO SAMEORIGIN and DENY            refuse  X-Frame-Options has conflicting values
 8. CSP 'none', own page               refuse  frame-ancestors does not allow https://bank.example
 9. CSP 'self' partner, partner        render  frame-ancestors allows every ancestor
10. CSP partner, with XFO DENY         render  frame-ancestors allows every ancestor; X-Frame-Options ignored
11. CSP partner, other on top          refuse  frame-ancestors does not allow https://other.example
12. CSP 'none' in a meta element       render  frame-ancestors in a meta element is ignored
13. CSP 'none', report-only            render  report-only, so the frame is only reported
14. CSP default-src 'none' only        render  no frame-ancestors, and default-src does not cover it
15. CSP *.bank.example, bank.example   refuse  frame-ancestors does not allow https://bank.example
16. CSP https:, any https site         render  frame-ancestors allows every ancestor
Rendered inside a page of other.example: cases 1, 6, 12, 13, 14, 16

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node framing_policy.mjs

Six setups let other.example render the page. Case 1 sends nothing. Case 6 meant to allow one partner with ALLOW-FROM, which browsers no longer understand, so the header vanished and the page opened to everyone: OWASP calls this failing open. Case 12 put the rule in a meta element and case 13 in a report-only policy, which reports and never blocks. Case 14 relied on default-src 'none', which says nothing about framing, and case 16 allowed the scheme https:, which is every site on HTTPS. The refusals are as instructive: case 5 failed because a stranger sat above a same-origin parent, and case 10 shows frame-ancestors winning over DENY.

A report-only policy next to DENY

Press Edit and add ['XFO DENY with CSP report-only', { xfo: 'DENY', cspReportOnly: "frame-ancestors 'none'" }, [OTHER]], to cases, then run it. The page is refused, and only because of DENY: the report-only policy reports and decides nothing, so X-Frame-Options still counts.

Step through cases 10 and 5 as the browser does:

Interactive · stage by stage How a browser decides whether a page may be framed Step through two pages that arrive in frames, one with frame-ancestors and one with only X-Frame-Options.
How a browser decides whether a page may be framed, step 8 of 8. Showing Page arrives in a frame, Has frame-ancestors?, Check every ancestor, Read X-Frame-Options, One valid value?, Render or refuse (case 5: refused). In focus: Render or refuse.X-Frame-Options ignoredPage arrives in a framenot a top-level pageHas frame-ancestors?enforced header onlyCheck every ancestorparent first, up to the topRead X-Frame-Optionsonly without frame-ancestorsOne valid value?DENY, or SAMEORIGIN for allRender or refusecase 5: refused

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Case 10 arrives. The bank's account page arrives in a frame on partner.example. Its response has frame-ancestors 'self' https://partner.example and X-Frame-Options DENY.
  2. An enforced policy. The policy is an enforced header, not a meta element or a report-only policy, so frame-ancestors decides and X-Frame-Options is set aside.
  3. Every ancestor. The browser checks each ancestor against the list, here only the parent, partner.example, which is listed. Had another site framed the partner's page, that site would have to be listed too.
  4. Rendered. Every ancestor matched, so the page renders, and the DENY is never read. Browsers that support frame-ancestors ignore X-Frame-Options when both are sent.
  5. Case 5 arrives. Another page arrives in a frame, with only X-Frame-Options SAMEORIGIN. Its parent is a bank page, and above that sits a page of other.example.
  6. No frame-ancestors. There is no enforced frame-ancestors, so the browser falls back to the HTML standard's check of X-Frame-Options.
  7. One value, every level. The header holds one valid value, SAMEORIGIN, so every ancestor up to the top must be the page's own origin. The parent is; other.example at the top is not.
  8. Refused. One ancestor failed, so the browser refuses to render the page. A same-origin parent does not help when a stranger sits above it.

Browsers have supported frame-ancestors for years, Chrome since version 40, Edge since 15, Firefox fully since 58 and Safari since 10, by MDN’s compatibility data. MDN still advises sending X-Frame-Options as well, for browsers that lack the directive; the current ones ignore it once frame-ancestors is there.

The rule in a meta element

<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'"> does nothing: the specification says the directive must be ignored when it arrives in a meta element, as case 12 showed. Send it as a response header, and test the header, not the page source.

Checkpoint

Check: How frame-ancestors and X-Frame-Options decide

  1. Question 1 of 3 A proxy adds X-Frame-Options DENY to a response that the app already sent with X-Frame-Options SAMEORIGIN, and there is no frame-ancestors. What does a current browser do when the page's own origin frames it?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: It refuses the frame, because the HTML standard blocks a response whose values conflict

    The HTML standard treats conflicting X-Frame-Options values as a reason to refuse, so two layers that each send their own value can break pages that should be framed. Send the header from one place.

  2. Question 2 of 3 A page sends frame-ancestors https://partner.example. partner.example frames it, and partner.example's page is itself framed by other.example. What happens?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: The browser refuses, because every ancestor must match and other.example does not

    A partner's page can be framed by someone else, so a check of the parent alone would let anyone wrap the partner. CSP Level 3 refuses as soon as one ancestor does not match.

  3. Question 3 of 3 A page puts frame-ancestors 'none' in a meta element and sends no framing header. Can another site frame it?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: Yes, because the directive is ignored in a meta element

    frame-ancestors works only in an enforced response header. In a meta element, in a report-only policy or left to default-src, it protects nothing.

Embedding on purpose: its own origin and named ancestors

Some pages must be embedded: a support widget, a booking form on partners’ sites, a status badge. Treat the embeddable part as a small app of its own. Serve it from an origin of its own, such as widget.example.com, so that the rest of the app keeps frame-ancestors 'none'; list the partners by origin, frame-ancestors https://partner-a.example https://partner-b.example; and keep what the embedded page can do small. An action that matters, such as paying or changing an account, should need a step the frame cannot click for the user, for example signing in again in a new window of its own.

Wildcards need care. https://*.example.com matches every subdomain but not example.com itself, which OWASP’s cheat sheet points out and case 15 of the simulator showed, so the bare host needs an entry of its own. A bare scheme such as https: or a * allows the whole web. And X-Frame-Options cannot list several origins at all; ASVS 5.0.0 calls it obsolete and asks for frame-ancestors instead. Which setup fits a page depends on two questions about it. Answer them for a page of your own app:

Interactive · answer and compare Which framing policy fits this page? Answer two questions about one page of yours. The table marks the setups that fit and says why the others do not.
Which pages need to show this page in a frame?
Does the page let one click do something that matters, such as pay, delete or grant consent?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five framing setups against what each lets in and keeps out (cases from the verdict program)
Setup Keeps other sites out Lets your own pages in Lets named partners in Holds in old browsers
frame-ancestors 'none' + DENY May frame the page: no page at all Good refused everywhere, even by your own pages (case 8) Poor your own pages are refused too Poor partners are refused Good DENY covers browsers without frame-ancestors
'self' + SAMEORIGIN May frame the page: pages of the same origin, at every level Good another origin at any level is refused (case 5) Good pages of the same origin may frame it Poor a partner is another origin Good SAMEORIGIN keeps other sites out; every ancestor since Chrome 61 and Firefox 59
Named origins, no XFO May frame the page: the listed origins, at every level Good only the list, and every ancestor is checked (case 11) Mixed only if 'self' is in the list Good each partner origin by name (case 9) Poor without X-Frame-Options, an old browser lets any site frame it
frame-ancestors https: May frame the page: any page served over HTTPS Poor any site on HTTPS may frame it (case 16) Good your pages are on HTTPS too Good and so is everyone else Poor nothing for old browsers
No framing header May frame the page: any page Poor any site may frame it (case 1) Good anyone may Good anyone may Poor nothing for any browser
How the answers decide
  • “Which pages need to show this page in a frame?” None, it is never embedded: adds a caveat to 'self' + SAMEORIGIN and Named origins, no XFO. These allow frames that nobody needs. Prefer 'none' with DENY, so even a page of your own cannot frame it.
  • “Which pages need to show this page in a frame?” Only pages of its own origin: rules out frame-ancestors 'none' + DENY. 'none' refuses your own pages as well, so the page that needs to frame it breaks.
  • “Which pages need to show this page in a frame?” Named partners' sites: rules out frame-ancestors 'none' + DENY and 'self' + SAMEORIGIN. A partner is another origin, so both refuse it. Name each partner in frame-ancestors instead.
  • “Which pages need to show this page in a frame?” Named partners' sites: adds a caveat to Named origins, no XFO. Old browsers that know only X-Frame-Options let any site frame the page, so keep its actions behind a confirmation the frame cannot click for the user.
  • “Does the page let one click do something that matters, such as pay, delete or grant consent?” Yes: rules out frame-ancestors https: and No framing header. Any site could frame the action and place it under a decoy, which is exactly what clickjacking needs.

In short: 'none' with DENY for pages nothing needs to embed, 'self' with SAMEORIGIN for pages your own app frames, named origins without X-Frame-Options for partners, and never a scheme or nothing on a page with actions.

Checkpoint

Check: Embedding on purpose

  1. Question 1 of 2 A page sends frame-ancestors https://*.example.com. Which of these ancestors is refused?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: https://example.com

    https://*.example.com stands for every host that ends with .example.com, at any depth, but not example.com itself. Case 15 of the simulator refused the bare host for this reason.

  2. Question 2 of 2 Your widget lets two partners frame it with frame-ancestors. A teammate adds X-Frame-Options SAMEORIGIN "for old browsers". What changes?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Current browsers ignore it, while old browsers that know only X-Frame-Options now refuse both partners

    X-Frame-Options cannot express a list of partners. On a page that partners embed, leave it out and rely on frame-ancestors, and keep the page's actions behind a step a frame cannot take for the user.

Sandbox the frames you embed

The same thinking applies in the other direction, when your page embeds someone else’s: a map, a video, an ad, a preview of HTML that a user wrote. The iframe sandbox: The sandbox attribute of an iframe. With no tokens it takes away scripts, forms, pop-ups, navigation of the top page and the frame's own origin; each token, such as allow-scripts, gives one power back. of iframe takes powers away from the framed page, and with no tokens at all MDN says it applies every restriction: no scripts, no forms, no pop-ups, no navigation of your page, and an origin of its own that matches nothing. Each token gives one power back, so give the fewest that the content needs. This program checks seven embeds of one shop by those rules:

Seven embeds checked against their sandbox tokens JavaScript · sandbox/sandbox_check.mjs
// What an iframe you embed may do, worked out from its sandbox tokens as MDN describes them, and what a check of the
// embed should flag. The embeds are data here; in a project the same check runs over the iframes of your templates.

function powers(tokens) {
  const has = (t) => tokens.includes(t);
  return {
    scripts: has('allow-scripts'),
    forms: has('allow-forms'),
    popups: has('allow-popups'),
    ownOrigin: has('allow-same-origin'),
    navigateTop: has('allow-top-navigation') ? 'any time' : has('allow-top-navigation-by-user-activation') ? 'after a click' : 'never',
  };
}

function flags(embed) {
  const found = [];
  const thirdParty = embed.src !== embed.page;
  if (embed.sandbox === null) {
    if (thirdParty) found.push('no sandbox: it keeps every power of its own origin');
  } else {
    const tokens = embed.sandbox.split(/\s+/).filter(Boolean);
    const p = powers(tokens);
    if (p.scripts && p.ownOrigin && !thirdParty) found.push('same origin with allow-scripts and allow-same-origin: it can remove its own sandbox');
    if (p.navigateTop === 'any time') found.push('allow-top-navigation: it can send your page elsewhere at any moment');
    if (tokens.includes('allow-popups-to-escape-sandbox')) found.push('its pop-ups run without the sandbox');
  }
  if (embed.userContent && !thirdParty) found.push('user content on your own origin: opened outside the frame, it runs unsandboxed');
  for (const feature of (embed.allow ?? '').split(/[;\s]+/).filter(Boolean)) {
    if (['camera', 'microphone', 'geolocation', 'payment'].includes(feature)) found.push(`allow grants ${feature}`);
  }
  return found;
}

const PAGE = 'https://shop.example';
const embeds = [
  { name: 'map, as pasted from the docs', src: 'https://maps.example', page: PAGE, sandbox: null, allow: 'geolocation' },
  { name: 'map, sandboxed', src: 'https://maps.example', page: PAGE, sandbox: 'allow-scripts allow-same-origin', allow: '' },
  { name: 'user HTML preview, own origin', src: PAGE, page: PAGE, sandbox: 'allow-scripts allow-same-origin', allow: '', userContent: true },
  { name: 'user HTML preview, scripts only', src: PAGE, page: PAGE, sandbox: 'allow-scripts', allow: '', userContent: true },
  { name: 'user HTML preview, own host', src: 'https://usercontent.example', page: PAGE, sandbox: 'allow-scripts', allow: '', userContent: true },
  { name: 'ad slot, generous', src: 'https://ads.example', page: PAGE, sandbox: 'allow-scripts allow-top-navigation allow-popups allow-popups-to-escape-sandbox', allow: '' },
  { name: 'ad slot, tight', src: 'https://ads.example', page: PAGE, sandbox: 'allow-scripts allow-popups allow-top-navigation-by-user-activation', allow: '' },
];

let flagged = 0;
for (const e of embeds) {
  const found = flags(e);
  if (found.length) flagged++;
  const p = e.sandbox === null ? null : powers(e.sandbox.split(/\s+/));
  const what = p
    ? `scripts ${p.scripts ? 'yes' : 'no'}, forms ${p.forms ? 'yes' : 'no'}, own origin ${p.ownOrigin ? 'yes' : 'no'}, navigates your page ${p.navigateTop}`
    : 'no sandbox';
  console.log(`${e.name}\n  ${what}`);
  for (const f of found.length ? found : ['nothing to flag']) console.log(`  - ${f}`);
}
console.log(`${flagged} of ${embeds.length} embeds flagged`);

Output

map, as pasted from the docs
  no sandbox
  - no sandbox: it keeps every power of its own origin
  - allow grants geolocation
map, sandboxed
  scripts yes, forms no, own origin yes, navigates your page never
  - nothing to flag
user HTML preview, own origin
  scripts yes, forms no, own origin yes, navigates your page never
  - same origin with allow-scripts and allow-same-origin: it can remove its own sandbox
  - user content on your own origin: opened outside the frame, it runs unsandboxed
user HTML preview, scripts only
  scripts yes, forms no, own origin no, navigates your page never
  - user content on your own origin: opened outside the frame, it runs unsandboxed
user HTML preview, own host
  scripts yes, forms no, own origin no, navigates your page never
  - nothing to flag
ad slot, generous
  scripts yes, forms no, own origin no, navigates your page any time
  - allow-top-navigation: it can send your page elsewhere at any moment
  - its pop-ups run without the sandbox
ad slot, tight
  scripts yes, forms no, own origin no, navigates your page after a click
  - nothing to flag
4 of 7 embeds flagged

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sandbox_check.mjs

Four were flagged. The map as pasted from a provider’s docs had no sandbox and granted geolocation through allow, the attribute that sets the frame’s Permissions Policy, within what the page itself is allowed. The user’s HTML preview served from the shop’s own origin with allow-scripts allow-same-origin is the combination MDN strongly discourages: the framed document can remove its own sandbox, which leaves it no safer than no sandbox at all. Even with scripts only it was flagged, because a page on your own origin that a user can open directly runs with no sandbox; the fix is the one that passed, the preview served from a host of its own. And the generous ad slot could navigate the shop’s page at any moment and open pop-ups outside the sandbox, while the tight one could navigate it only after a click. Add referrerpolicy to send less of your address, and loading="lazy" so that a frame far down the page loads only when it comes near.

Sandboxing is the embedding side of the same rule: a frame should have the powers its job needs and nothing more.

Checkpoint

Check: Sandboxing the frames you embed

  1. Question 1 of 2 A preview of HTML that users write is served from your own origin in an iframe with sandbox set to allow-scripts allow-same-origin. What is the problem?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The framed document can remove its own sandbox, so it is no safer than no sandbox at all

    Serve user-written content from an origin of its own and give its frame only allow-scripts. On your own origin it can lift the sandbox, and opened directly it runs with no sandbox at all.

  2. Question 2 of 2 An iframe has a sandbox attribute with no tokens. What may the framed page do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Show its content, with no scripts, forms, pop-ups or navigation of your page, and with an origin that matches nothing

    Start from an empty sandbox and add the fewest tokens the content needs, such as allow-scripts for a map, and allow-top-navigation-by-user-activation rather than allow-top-navigation for an ad.

Frame-busting scripts and SameSite are extras, not the defence

Two older answers still turn up in code and in advice, and both help less than they seem to:

Extra When it helps Why it is not enough
A frame-busting script that tries to escape frames In very old browsers without either header The framing page can switch scripts off in the frame with sandbox, and then the script never runs; OWASP lists other public ways around it
SameSite cookies The action needs a login, and the frame is on another site Same-site pages, such as a sibling subdomain, still get the cookie; an action without login is not covered; browsers differ in their defaults
A confirmation dialog As one more step for the user Some sandboxed frames suppress dialogs, and the call then returns false without asking

MDN calls SameSite an additional, partial mitigation, and OWASP says it should not be the only protection. OWASP also describes a newer variant across separate windows, where no frame is involved and framing headers cannot help, and asks for additional safeguards on sensitive actions there. For the framed kind the core stays the same: the response header decides, and the test is an assertion on that header.

Test it as you test the baseline: request each kind of page on your own environments and check frame-ancestors, and X-Frame-Options where a page uses it, on every response, error pages included. ZAP’s passive rule 10020 flags a missing header, multiple X-Frame-Options entries, one set in a meta element and a malformed value, and the module on security testing runs it in CI. Frame only pages you own or have written permission to test.

Key idea

Frame-busting scripts, SameSite cookies and dialogs are extras: the defence is a frame-ancestors header on every response, checked by a test.

Checkpoint

Check: Why frame busting and SameSite are extras

  1. Question 1 of 2 Why does a script that tries to break out of frames not protect a page reliably?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The framing page can switch scripts off in the frame with the sandbox attribute, and then the script never runs

    A defence that runs inside the framed page can be switched off by the page that frames it. A response header is read by the browser before the page runs at all.

  2. Question 2 of 2 Your session cookie is SameSite=Strict. A page on blog.example.com, run by another team, frames app.example.com/settings. Is the framed page signed in?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because blog.example.com is the same site, so the Strict cookie goes with the frame

    SameSite helps only when the framing page is on another site and the action needs the login. A sibling subdomain is the same site, which is why OWASP says SameSite should never be the only protection.

Practise: framing headers for six pages

Write the framing headers of six kinds of page, from an OAuth consent screen to a widget for partners. The sample tests judge each answer with the rules of this lesson, once as a current browser does and once as an old browser that knows only X-Frame-Options:

Exercise · Medium · JavaScript, Python

Framing headers for six kinds of page

Write framingHeaders(page) in framing.mjs (Python: framing_headers(page) in framing.py). It returns the framing headers of one kind of page as an object (a dict in Python), header name to value. The sample tests check each answer with the browser rules of the lesson, in a current browser and in an old one that knows only X-Frame-Options:

  • **'consent', 'download' and 'error'**: no page may frame them, not even your own. Send frame-ancestors and an X-Frame-Options that old browsers follow too.
  • **'app'**, the signed-in app at https://app.example.com: only its own origin may frame it, in old browsers too.
  • **'widget'** at https://widget.example.com: only the origins in PARTNERS may frame it. Send no X-Frame-Options, which cannot name them and would make old browsers refuse every partner.
  • **'marketing'** at https://www.example.com: its own origin, https://example.com and every https subdomain of example.com may frame it, and no X-Frame-Options.

The Content-Security-Policy holds only the frame-ancestors directive. Any other kind of page is an error: throw (Python: raise ValueError).

JavaScript · Starter code · framing.mjs

/** The origins that may embed the widget. */
export const PARTNERS = ['https://partner-a.example', 'https://partner-b.example'];

/**
 * The framing headers of one kind of page: 'consent', 'download', 'error', 'app', 'widget' or 'marketing', as an
 * object of header name to value. Any other kind throws.
 */
export function framingHeaders(page) {
  // Starter: the same legacy header for every page.
  return { 'X-Frame-Options': 'SAMEORIGIN' };
}
The sample tests · framing.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { framingHeaders } from './framing.mjs';

const PAGES = { consent: 'https://login.example.com', download: 'https://app.example.com', error: 'https://app.example.com', app: 'https://app.example.com', widget: 'https://widget.example.com', marketing: 'https://www.example.com' };

// The lesson's rules: frame-ancestors checks every ancestor; X-Frame-Options counts only without it (old browsers:
// only X-Frame-Options). Ancestors are listed from the parent up to the top.
function matches(source, ancestor, self) {
  if (source === "'self'") return ancestor === self;
  if (/^[a-z]+:$/.test(source)) return ancestor.startsWith(`${source}//`);
  const [scheme, host] = source.split('://');
  const [aScheme, aHost] = ancestor.split('://');
  if (scheme !== aScheme) return false;
  return host.startsWith('*.') ? aHost.endsWith(host.slice(1)) : aHost === host;
}
function xfoAllows(xfo, self, ancestors) {
  if (!xfo) return true;
  const v = xfo.trim().toLowerCase();
  if (v === 'deny') return false;
  if (v === 'sameorigin') return ancestors.every((a) => a === self);
  return true;
}
function allows(page, ancestors, { old = false } = {}) {
  const h = framingHeaders(page);
  const self = PAGES[page];
  const fa = /frame-ancestors([^;]*)/.exec(h['Content-Security-Policy'] ?? '');
  if (fa && !old) {
    const sources = fa[1].trim().split(/\s+/);
    return sources[0] !== "'none'" && ancestors.every((a) => sources.some((s) => matches(s, a, self)));
  }
  return xfoAllows(h['X-Frame-Options'], self, ancestors);
}

const OTHER = 'https://other.example';

test('consent, download and error pages refuse every frame, in old browsers too', () => {
  for (const page of ['consent', 'download', 'error']) {
    assert.ok(!allows(page, [PAGES[page]]), `${page}: not even its own origin`);
    assert.ok(!allows(page, [OTHER]), `${page}: not another site`);
    assert.ok(!allows(page, [PAGES[page]], { old: true }), `${page}: an old browser refuses it too`);
  }
});

test('the app may be framed by its own origin only, at every level', () => {
  assert.ok(allows('app', ['https://app.example.com']));
  assert.ok(!allows('app', ['https://www.example.com']), 'a sibling host is another origin');
  assert.ok(!allows('app', ['https://app.example.com', OTHER]), 'every ancestor counts, the top included');
  assert.ok(allows('app', ['https://app.example.com'], { old: true }) && !allows('app', [OTHER], { old: true }), 'old browsers keep the same rule');
});

test('the widget may be framed by the partners only, and sends no X-Frame-Options', () => {
  assert.ok(allows('widget', ['https://partner-a.example']) && allows('widget', ['https://partner-b.example']));
  assert.ok(!allows('widget', [OTHER]) && !allows('widget', ['https://partner-a.example', OTHER]));
  assert.ok(!('X-Frame-Options' in framingHeaders('widget')), 'X-Frame-Options cannot name partners');
});

test('the marketing page allows example.com and its https subdomains, and nothing else', () => {
  for (const a of ['https://www.example.com', 'https://example.com', 'https://blog.example.com']) assert.ok(allows('marketing', [a]), a);
  for (const a of ['http://blog.example.com', 'https://example.com.other.example', OTHER]) assert.ok(!allows('marketing', [a]), a);
  assert.ok(!('X-Frame-Options' in framingHeaders('marketing')));
  assert.throws(() => framingHeaders('video'));
});

Python · Starter code · framing.py

# The origins that may embed the widget.
PARTNERS = ["https://partner-a.example", "https://partner-b.example"]


def framing_headers(page):
    """The framing headers of one kind of page: 'consent', 'download', 'error', 'app', 'widget' or 'marketing', as
    a dict of header name to value. Any other kind raises ValueError."""
    # Starter: the same legacy header for every page.
    return {"X-Frame-Options": "SAMEORIGIN"}
The sample tests · test_framing.py
import re

from framing import framing_headers

PAGES = {
    "consent": "https://login.example.com", "download": "https://app.example.com", "error": "https://app.example.com",
    "app": "https://app.example.com", "widget": "https://widget.example.com", "marketing": "https://www.example.com",
}
OTHER = "https://other.example"


def matches(source, ancestor, self):
    if source == "'self'":
        return ancestor == self
    if re.fullmatch(r"[a-z]+:", source):
        return ancestor.startswith(source + "//")
    scheme, host = source.split("://")
    a_scheme, a_host = ancestor.split("://")
    if scheme != a_scheme:
        return False
    return a_host.endswith(host[1:]) if host.startswith("*.") else a_host == host


def xfo_allows(xfo, self, ancestors):
    if not xfo:
        return True
    value = xfo.strip().lower()
    if value == "deny":
        return False
    if value == "sameorigin":
        return all(a == self for a in ancestors)
    return True


def allows(page, ancestors, old=False):
    """The lesson's rules: frame-ancestors checks every ancestor, X-Frame-Options counts only without it (old
    browsers: only X-Frame-Options). Ancestors are listed from the parent up to the top."""
    headers = framing_headers(page)
    self = PAGES[page]
    fa = re.search(r"frame-ancestors([^;]*)", headers.get("Content-Security-Policy", ""))
    if fa and not old:
        sources = fa.group(1).split()
        return sources[0] != "'none'" and all(any(matches(s, a, self) for s in sources) for a in ancestors)
    return xfo_allows(headers.get("X-Frame-Options"), self, ancestors)


def test_consent_download_and_error_pages_refuse_every_frame_in_old_browsers_too():
    for page in ("consent", "download", "error"):
        assert not allows(page, [PAGES[page]]), f"{page}: not even its own origin"
        assert not allows(page, [OTHER]), f"{page}: not another site"
        assert not allows(page, [PAGES[page]], old=True), f"{page}: an old browser refuses it too"


def test_the_app_may_be_framed_by_its_own_origin_only_at_every_level():
    assert allows("app", ["https://app.example.com"])
    assert not allows("app", ["https://www.example.com"]), "a sibling host is another origin"
    assert not allows("app", ["https://app.example.com", OTHER]), "every ancestor counts, the top included"
    assert allows("app", ["https://app.example.com"], old=True) and not allows("app", [OTHER], old=True)


def test_the_widget_may_be_framed_by_the_partners_only_and_sends_no_x_frame_options():
    assert allows("widget", ["https://partner-a.example"]) and allows("widget", ["https://partner-b.example"])
    assert not allows("widget", [OTHER]) and not allows("widget", ["https://partner-a.example", OTHER])
    assert "X-Frame-Options" not in framing_headers("widget"), "X-Frame-Options cannot name partners"


def test_the_marketing_page_allows_example_com_and_its_https_subdomains_and_nothing_else():
    for a in ("https://www.example.com", "https://example.com", "https://blog.example.com"):
        assert allows("marketing", [a]), a
    for a in ("http://blog.example.com", "https://example.com.other.example", OTHER):
        assert not allows("marketing", [a]), a
    assert "X-Frame-Options" not in framing_headers("marketing")
    try:
        framing_headers("video")
    except ValueError:
        return
    raise AssertionError("an unknown kind of page must raise ValueError")
A hint

Four answers cover the six pages: 'none' with DENY, 'self' with SAMEORIGIN, the partners' origins, and a list for the marketing page. For that list, remember that https://*.example.com matches the subdomains of example.com but not example.com itself, so the bare host needs an entry of its own.

The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Security Headers Checker Paste the headers of a page you own and see its frame-ancestors and X-Frame-Options checked the way browsers parse them. Content Security Policy (CSP) Generator Draft a Content-Security-Policy whose frame-ancestors lists exactly the origins that may embed your page.

Then work through the self-check. It asks one question at a time and begins with the idea you are least sure of:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What is clickjacking also called, and which weakness does OWASP map it to?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: UI redress, CWE-1021 improper restriction of rendered UI layers or frames

    Clickjacking is also called UI redress: another site dresses its own interface over yours, so the user sees one page and clicks on another. Its CWE is 1021.

  2. Question 2 of 10 An internal tool with a one-click Refund now button is reachable only from the office network. Does it need framing protection?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Yes, because any page a staff member opens can frame it from their browser, which is inside the network

    Clickjacking runs in the victim's own browser. A page on an internal network is framed as easily as a public one when someone inside opens the framing page.

  3. Question 3 of 10 Which values does X-Frame-Options define in the HTML standard? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 3 of 10

    Answer:

    • DENY
    • SAMEORIGIN

    Only DENY and SAMEORIGIN are valid. Anything else alone counts as no header, and that is why ALLOW-FROM fails open.

  4. Question 4 of 10 A page sends frame-ancestors 'self' and X-Frame-Options DENY. A page of its own origin frames it. What does a current browser do?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: It renders the page, because frame-ancestors overrides X-Frame-Options

    CSP Level 3 makes frame-ancestors override X-Frame-Options so that sites can send both. Old browsers that lack the directive read DENY instead.

  5. Question 5 of 10 Where should a page that partners embed be served from?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: From an origin of its own, so that the rest of the app keeps frame-ancestors 'none'

    Give the embeddable part its own origin, name the partners in its frame-ancestors, and keep its actions small.

  6. Question 6 of 10 A widget has a Cancel subscription button. Why is frame-ancestors * the wrong setting for it?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Any site could frame the button under a decoy, which is all clickjacking needs

    Name each partner instead, and put actions that matter behind a step the frame cannot take for the user, such as signing in again in a window of its own.

  7. Question 7 of 10 An ad frame has sandbox set to allow-scripts allow-top-navigation. What can the ad do, and what would you change?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Send your page elsewhere at any moment, so use allow-top-navigation-by-user-activation, which needs a click

    Each token is a power. Give the frame the narrowest one that does the job: navigation only after a user's click, pop-ups that stay sandboxed.

  8. Question 8 of 10 Which attribute of iframe grants a framed map access to the user's location?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: allow, the frame's Permissions Policy

    MDN describes allow as a further restriction on top of the Permissions-Policy header. Grant a feature only when the embed needs it, as the checked map did not.

  9. Question 9 of 10 How does MDN describe SameSite cookies as a defence against clickjacking?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: As an additional, partial mitigation

    SameSite keeps the session cookie off cross-site frames, which can leave the framed page signed out. It does nothing about same-site frames or actions that need no login.

  10. Question 10 of 10 A framed page asks window.confirm() before a sensitive action. Is that a reliable defence?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: No, because some sandboxed frames suppress dialogs, and the call then returns false without asking

    A dialog is one more step for an honest user, not a defence. The header that refuses the frame is.

Interview questions

Warm-up: what is clickjacking, and what is the main defence?

Clickjacking is showing a page of your site inside a frame on another site, where the user does not notice it, lined up under something harmless, so that a real click or keystroke acts on your page: a payment, a deleted account, a granted permission. It needs only that your page may be framed, so the main defence is a response header that says who may frame it: Content-Security-Policy: frame-ancestors 'none', or 'self', or a list of named origins, with X-Frame-Options: DENY or SAMEORIGIN as a fallback for old browsers. It must be a header, not a meta element, and it must be on every response that renders a page. Frame-busting scripts and SameSite cookies help at the edges, but neither replaces the header.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to staff

    A partner wants to embed your payment confirmation page in a frame inside their own checkout. How do you decide, and if you agree, what do you configure and what do you change in the page?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to senior

    Your site sends X-Frame-Options ALLOW-FROM https://partner.example, and its pages carry frame-ancestors in a meta tag. In a test of your own site, the account page loads inside a frame on another origin. Explain what the browser did with each rule, and the fix.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is clickjacking, and what does it need from your site?

    Another site shows your page in a frame, lined up under something harmless, so the user's genuine click acts on your page (CWE-1021). It needs only that your page may be framed. The server never sees that the click came through someone else's page, so the defence belongs in the response.

  2. How does the browser read frame-ancestors and X-Frame-Options together?

    An enforced frame-ancestors header checks every ancestor, parent up to the top, and overrides X-Frame-Options. It is ignored in a meta element, does not fall back to default-src, and a report-only policy decides nothing. X-Frame-Options has only DENY and SAMEORIGIN; a lone unknown value such as ALLOW-FROM counts as no header, so the page fails open.

  3. Which pages should refuse framing, and what is the default?

    Every page where a click or a few keystrokes does something that matters: paying, deleting, changing an email or password, an OAuth consent screen, signing in. Make "no page may be framed" the default, with frame-ancestors on every response (ASVS 5.0.0 3.4.6), and configure the few embeddable pages as exceptions.

  4. How do you let named partners embed a page safely?

    Serve the embeddable part from an origin of its own, keep frame-ancestors 'none' for the rest of the app, list partners by origin, and keep what the embedded page can do small. https://*.example.com does not match example.com itself, a bare scheme such as https: or * allows the whole web, and X-Frame-Options cannot list several origins.

  5. How does the sandbox attribute protect a page that embeds someone else's frame?

    With no tokens it applies every restriction: no scripts, forms, pop-ups or navigation of your page, and an origin of its own. Each token gives one power back, so grant the fewest the content needs. allow-scripts together with allow-same-origin lets the framed document remove its own sandbox, so never use that pair for content served from your own origin.

  6. Why are frame-busting scripts, SameSite cookies and confirmation dialogs not the defence?

    A framing page can switch scripts off in the frame with sandbox, so the buster never runs. SameSite does not cover same-site pages such as a sibling subdomain, or actions that need no login (MDN calls it a partial mitigation). Some sandboxed frames suppress dialogs. The defence is the header on every response, asserted by a test.

Key takeaways

  • Clickjacking needs only that another site may frame your page, so every page with an action that matters refuses framing, and embedding is a configured exception.
  • frame-ancestors in an enforced header checks every ancestor and overrides X-Frame-Options; in a meta element, in a report-only policy or left to default-src it protects nothing.
  • X-Frame-Options knows only DENY and SAMEORIGIN; ALLOW-FROM is ignored and fails open, and two different values, one of them DENY or SAMEORIGIN, make the browser refuse.
  • Pages that must be embedded live on an origin of their own and name each partner; wildcards need the bare host added, and a scheme or * allows the whole web.
  • Frames you embed get the fewest sandbox tokens they need, never allow-scripts with allow-same-origin for content on your own origin.
  • Frame-busting scripts, SameSite cookies and dialogs are extras; the defence is the header, asserted by a test on every kind of response.

Terms in this lesson

clickjacking
Showing a page of one site inside a frame on another site, where the user does not notice it, lined up under something harmless, so that a real click or keystroke acts on the framed page. Also called UI redress (CWE-1021).
frame ancestor
A document that contains a frame, directly or further up: the parent, its parent and so on to the top-level page. frame-ancestors and X-Frame-Options SAMEORIGIN check every one of them, not only the parent.
iframe sandbox
The sandbox attribute of an iframe. With no tokens it takes away scripts, forms, pop-ups, navigation of the top page and the frame's own origin; each token, such as allow-scripts, gives one power back.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.