Why JavaScript Frame-Busting Doesn't Stop Clickjacking
Published May 2025 · sourced from OWASP
Frame-busting is the oldest clickjacking countermeasure. The idea is simple: a script in your page detects when it's loaded inside an iframe and “busts out” by redirecting the parent window.
if (top != self) {
top.location = self.location;
}For about two years after clickjacking was discovered in 2008, this worked. Then researchers published systematic bypasses. Today, frame-busting is dead. Relying on it creates a false sense of security.
How frame-busting is bypassed
Every browser-based bypass exploits the same flaw: frame-busting depends on JavaScript running inside the framed page. If the attacker can prevent that JavaScript from executing, or interfere with the redirect, the defense fails.
HTML5 sandbox attribute
The sandbox attribute on an iframe restricts what the framed page can do. Without flags, it blocks scripts entirely.
<iframe src="https://victim.com" sandbox></iframe>No scripts run. No frame-busting. The page renders inside the iframe, defenseless.
To allow scripts but still block the bust-out, the attacker omits allow-top-navigation:
<iframe src="https://victim.com" sandbox="allow-forms allow-scripts"></iframe>The victim's JavaScript runs, but top.location assignments are ignored. This works in all modern browsers that support the sandbox attribute.
Double framing
If the victim page is nested two frames deep (attacker → intermediate → victim), accessing parent.location from the victim frame becomes a cross-origin security violation under the descendant frame navigation policy. The browser blocks the assignment and the bust-out fails.
<!-- Attacker's top page -->
<iframe src="intermediate.html"></iframe>
<!-- intermediate.html -->
<iframe src="https://victim.com"></iframe>onBeforeUnload handler
The attacker registers an onbeforeunload handler on their top-level page. When the victim's frame-busting script tries to navigate the parent, the handler fires and the browser shows a confirmation dialog. Users click Cancel almost every time.
<script>
window.onbeforeunload = function() {
return "Please stay on this page.";
};
</script>
<iframe src="https://victim.com"></iframe>No-content flushing
A more aggressive version of the onBeforeUnload bypass that doesn't require user interaction. The attacker repeatedly submits navigation requests to an endpoint returning 204 No Content. Each 204 flushes the browser's navigation pipeline, canceling any pending redirect -- including the victim's frame-busting attempt.
var preventbust = 0;
window.onbeforeunload = function() { preventbust++; };
setInterval(function() {
if (preventbust > 0) {
preventbust = 2;
window.top.location = 'http://nocontent204.com';
}
}, 1);Source: OWASP Clickjacking Defense Cheat Sheet
Firefox designMode
The parent page enters designMode, which disables scripts in all child frames:
document.designMode = "on";With scripts disabled, the frame-busting code never runs. This technique was historically effective in Firefox; support varies in current versions.
CSP sandbox directive
The attacker can serve their framing page with a Content-Security-Policy: sandbox header, applying sandbox restrictions to every child frame -- even if the victim's server sets no sandbox policy. Scripts in the victim frame are blocked by the parent's CSP.
Why HTTP headers work when JavaScript doesn't
X-Frame-Options and Content-Security-Policy: frame-ancestors are enforced by the browser's network layer, not by JavaScript in the page. The browser checks these headers before rendering begins. If the header says DENY, the browser refuses to load the page in a frame. No script execution needed. No way for the attacker to interfere.
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 recommended as defense in depth. In practice, the headers are the only layer that matters. If the browser supports X-Frame-Options or CSP frame-ancestors, JavaScript adds nothing -- the headers block framing before the script ever runs. If the browser is old enough to lack header support, it's also old enough to lack the sandbox attribute, but other bypasses (double framing, onBeforeUnload) still work.
The real risk: a developer adds frame-busting, tests it quickly in dev tools, sees it works, and skips the headers. That is not defense in depth. That is a single fragile layer.
What to use instead
Set one of these HTTP headers on every response:
Content-Security-Policy: frame-ancestors 'none'For broader browser support:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY