Why JavaScript Frame-Busting Doesn't Stop Clickjacking
Published May 2026 · Updated October 2026 · sourced from OWASP
Frame-busting is an early clickjacking countermeasure. A script in your page detects that it is loaded inside an iframe and “busts out” by redirecting the top window.
if (top != self) {
top.location = self.location;
}Clickjacking got its name in 2008. In 2010, researchers at Stanford and Carnegie Mellon surveyed the Alexa Top 500 sites: only 14% used any frame-busting, and the researchers found a way around every defense they encountered. Don't rely on it. A bust-out script is one layer that the attacker's page can switch off, and some browsers now block the bust-out itself.
How frame-busting is bypassed
Most bypasses exploit the same flaw: frame-busting depends on JavaScript running inside the framed page, and the framing page controls the environment that script runs in. If the attacker can stop the script or block the redirect, the defense fails. Some of these techniques still work today. Others are historical, and each section says which.
HTML5 sandbox attribute (works today)
The sandbox attribute on an iframe restricts what the framed page can do. With no tokens, it blocks scripts entirely.
<iframe src="https://victim.com" sandbox></iframe>No scripts run, so no frame-busting code runs, and the page renders inside the iframe. (A page that hides its content until a script confirms it isn't framed fails safer. See the next sections.)
To let scripts run but still block the bust-out, the attacker leaves out allow-top-navigation:
<iframe src="https://victim.com" sandbox="allow-forms allow-scripts"></iframe>The victim's JavaScript runs, but the top.location assignment is blocked. A sandboxed frame can't navigate the top window unless the sandbox includes allow-top-navigation or allow-top-navigation-by-user-activation. This is standard HTML behavior and the most dependable bypass on this list.
CSP sandbox directive (works today)
The attacker can serve their framing page with a Content-Security-Policy: sandbox header. Sandbox flags set on a document are inherited by the frames inside it, so the victim frame is restricted even though the victim's server sets no sandbox policy. It is the same technique as the attribute, delivered through a header.
Browsers block the bust-out too
Chrome blocks a cross-origin iframe from navigating the top window unless the frame has had a user gesture. Google announced this “framebusting intervention” around Chrome 64 (2018) to stop ads and embeds from hijacking the page. It is also what a frame-buster does. Chromium's own tests call it non-standard, unspecified behavior, so don't assume other browsers match.
The result: in Chrome, a plain bust-out from a cross-origin frame does nothing until the user has already clicked inside the frame, which is the click the attacker wanted. The sandbox keyword allow-top-navigation-by-user-activation works the same way, and an attacker simply omits it. Browser interventions like this make frame-busting less effective, not more.
Double framing (historical)
The 2010 paper showed that if the victim page is nested two frames deep (attacker → intermediate → victim), a script that assigns parent.location fails. The parent is now the attacker's intermediate frame, and the browsers of the time treated that navigation as a cross-origin security violation. It only affected scripts that target parent. Scripts that assign top.location, like the one above, aren't stopped by double framing, and the sandbox bypass works regardless. Treat this as a historical technique.
<!-- Attacker's top page -->
<iframe src="intermediate.html"></iframe>
<!-- intermediate.html -->
<iframe src="https://victim.com"></iframe>onBeforeUnload handler (weak today)
The attacker registers an onbeforeunload handler on their top-level page. When the victim's script tries to navigate the top window, the handler fires and the browser asks the user to confirm leaving. In 2010 the attacker could write the prompt (the paper used “Do you want to exit PayPal?”), which made users likely to cancel. Modern browsers ignore custom text and show a generic prompt, and they only show it if the user has interacted with the page. That makes this a weak bypass today.
<script>
window.onbeforeunload = function() {
return "Please stay on this page.";
};
</script>
<iframe src="https://victim.com"></iframe>No-content (204) flushing (historical)
A variant that doesn't need the user to cancel anything. The attacker's handler counts navigation attempts, and a timer keeps navigating the top window to an endpoint that returns 204 No Content. A 204 response is effectively a no-op, but it flushes the browser's navigation pipeline and cancels the pending redirect, including the victim's bust-out. It was demonstrated in 2010 against Chrome, Firefox and Internet Explorer. We found no recent confirmation that it works in current browsers, and the user-gesture rule above blocks the cross-origin bust-out in Chrome anyway. Treat it as historical.
var prevent_bust = 0;
window.onbeforeunload = function() { prevent_bust++; };
setInterval(function() {
if (prevent_bust > 0) {
prevent_bust -= 2;
window.top.location = 'https://attacker.example/204';
}
}, 1);Source: OWASP Clickjacking Defense Cheat Sheet
XSS filter abuse (historical)
Browser XSS filters, Internet Explorer 8's filter and Chrome's XSS Auditor, could be tricked into disabling the frame-busting script. The attacker put the script's source in a URL parameter of the framed page, and the filter mistook the real script for injected code and blocked it. Chrome removed the XSS Auditor in version 78 (2019), citing information leaks and widely known bypasses, so this no longer works in current Chromium-based browsers.
Firefox designMode (historical)
In 2010, researchers showed that the framing page could set designMode, which disabled JavaScript in the frames inside it (reported for Firefox and Internet Explorer 8):
document.designMode = "on";With scripts disabled, the frame-busting code never runs. We couldn't find evidence that this works in current browsers. Treat it as historical.
Why HTTP headers work when JavaScript doesn't
X-Frame-Options and Content-Security-Policy: frame-ancestors are enforced by the browser when it receives the response, before any script in the page runs. If the policy forbids framing, the browser refuses to render the page in the frame. The framed page needs no script, and the framing page has no way to turn the check off.
This is the core difference: HTTP headers restrict the browser, not the page. JavaScript frame-busting restricts the page, and the attacker controls everything around the page.
“What about defense in depth?”
Adding frame-busting on top of headers is sometimes suggested as defense in depth, and OWASP still lists a frame-breaker as a fallback for legacy browsers. In practice it adds almost nothing. Current browsers support X-Frame-Options and CSP frame-ancestors, and the headers block the framing before the script runs. The only browsers a frame-buster could help are ones too old to support either header.
If you do add one, use the fail-closed version from the Stanford paper, which OWASP lists as its “best-for-now” legacy script. It hides the page until a script confirms the page is top-level, so blocked scripts leave a blank page instead of a clickable one:
<!-- in <head> -->
<style id="antiClickjack">body{display:none !important;}</style>
<script>
if (self === top) {
var antiClickjack = document.getElementById("antiClickjack");
antiClickjack.parentNode.removeChild(antiClickjack);
} else {
top.location = self.location;
}
</script>It is a fallback, not a replacement for the headers. Visitors with JavaScript disabled see a blank page, and the authors of the paper warned that it might fall to attacks nobody has found yet.
The real risk is a developer who adds a naive frame-buster like the first snippet above, sees it work in a quick test, and skips the headers. That version fails open: when its script is blocked, the page is fully usable inside the frame. It is a single fragile layer, not defense in depth.
What to use instead
Set this HTTP header on every response:
Content-Security-Policy: frame-ancestors 'none'To also cover browsers that don't support frame-ancestors, send X-Frame-Options too. Browsers that enforce frame-ancestors ignore X-Frame-Options, so keep the two consistent:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYSend them as real HTTP headers. frame-ancestors is ignored in a <meta> tag, and a Content-Security-Policy-Report-Only header reports violations without blocking anything.