Lab 1: Basic clickjacking with CSRF token protection
1. Executive Summary
Vulnerability: Clickjacking (UI Redressing).
Description: The application relies entirely on CSRF (Cross-Site Request Forgery) tokens to protect state-changing actions like “Delete Account”. While this prevents an attacker from forging a background request, the application fails to restrict whether it can be embedded within an <iframe> on an external site. This allows an attacker to load the target page invisibly over a decoy website, tricking the victim into clicking the hidden “Delete Account” button.
Impact: Unintended State Changes. Because the victim clicks the real button within the invisible iframe, the browser automatically includes their valid session cookies and the CSRF token. This bypasses the CSRF protection and forces the victim to delete their own account.
2. The Attack
Objective: Trick the victim into clicking the “Delete Account” button by overlaying an invisible iframe on top of a decoy “Click Me” button.
- Reconnaissance:
- I logged in as
wiener. - I inspected the
/my-accountpage and noticed the “Delete Account” button. - I checked the HTTP response headers for the page. It was missing both
X-Frame-OptionsandContent-Security-Policy(specifically theframe-ancestorsdirective), meaning the page could be framed by any domain.
- I logged in as
- Payload Construction:
- I went to the Exploit Server and created a malicious HTML page.
- I used CSS to create two overlapping layers.
- Layer 1 (The Decoy): A visible
<div>containing the text “Click me”, placed atz-index: 1(the bottom layer). - Layer 2 (The Trap): An
<iframe>loading the target/my-accountpage. I set itsz-index: 2(so it sat on top of the decoy), positioned it absolutely, and setopacity: 0.1so I could see it faintly.
- Alignment & Exploitation:
- I adjusted the
topandleftpixel values of the decoy<div>until the “Click me” text sat perfectly underneath the faint “Delete account” button in the iframe. - Once aligned, I changed the iframe’s opacity to
0.0001, making it completely invisible. - I delivered the exploit to the victim. When the victim clicked the visible “Click me” text, their cursor actually registered a click on the invisible “Delete account” button layered directly above it.
- I adjusted the
3. Code Review
This section analyzes why the application is vulnerable. Unlike logic bugs, Clickjacking is an infrastructure/header misconfiguration.
Vulnerability Analysis (Explanation):
Clickjacking vulnerabilities exist because the server fails to instruct the browser to block framing. Modern web frameworks often enable these protections by default, meaning developers must actively misconfigure or disable them for the vulnerability to exist.
Java (Spring Security)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/my-account").authenticated()
.and()
// VULNERABLE: The developer explicitly disabled frame options
// allowing the site to be embedded in external iframes.
.headers().frameOptions().disable();
}
}
Technical Flow & Syntax Explanation:
.headers().frameOptions().disable(): By default, Spring Security automatically injects theX-Frame-Options: DENYheader into every HTTP response. If a developer needs to frame a specific widget (e.g., a YouTube player or an internal dashboard), they sometimes lazily disable the protection globally using this method. This strips the header, making every page—including sensitive account pages—vulnerable to UI redressing.
C# (ASP.NET Core)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... other middleware
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// VULNERABLE: Missing Security Headers Middleware.
// ASP.NET Core does not inject X-Frame-Options or CSP headers by default.
// The developer relied solely on app.UseEndpoints() and AntiForgery tokens.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Technical Flow & Syntax Explanation:
- Missing Middleware: Unlike Spring, ASP.NET Core does not automatically apply anti-framing headers out-of-the-box in the standard middleware pipeline. If the developer does not explicitly add custom middleware or use a library (like
NetEscapades.AspNetCore.SecurityHeaders) to appendX-Frame-Options, the responses are served without framing restrictions, leaving the UI defenseless.
Mock PR Comment
The /my-account page is currently vulnerable to Clickjacking. The HTTP responses are missing the X-Frame-Options and Content-Security-Policy headers. While the CSRF tokens protect against cross-site background requests, they do not prevent an attacker from embedding the UI in a malicious iframe and redressing it.
Recommendation: Enforce anti-framing headers globally. Set X-Frame-Options: DENY (or SAMEORIGIN) and configure Content-Security-Policy: frame-ancestors 'none'; to instruct modern browsers to block rendering if the page is embedded.
4. The Fix
Explanation of the Fix:
We must instruct the victim’s browser to refuse to render the page if it is loaded inside an iframe.
Secure Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ... auth setup
.headers()
// SECURE: Ensures X-Frame-Options: SAMEORIGIN is sent
.frameOptions().sameOrigin()
.and()
// SECURE: The modern approach using CSP
.contentSecurityPolicy("frame-ancestors 'self'");
}
}
Technical Flow & Syntax Explanation:
.frameOptions().sameOrigin(): Instructs the browser that this page can only be framed by pages sharing the exact same origin (domain, protocol, port). It blocks attackers onevil.com..contentSecurityPolicy("frame-ancestors 'self'"): This is the modern standard that supersedesX-Frame-Options. It provides more granular control and is respected by all modern browsers.
Secure C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// SECURE: Adding custom middleware to inject headers on every response
app.Use(async (context, next) =>
{
// Legacy Support
context.Response.Headers.Add("X-Frame-Options", "DENY");
// Modern Standard Support
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none'");
await next();
});
app.UseRouting();
// ...
}
Technical Flow & Syntax Explanation:
context.Response.Headers.Add(...): This intercepts the HTTP pipeline and attaches the security headers to the outgoing response before it reaches the client.DENY/'none'strictly forbids framing anywhere, providing the maximum level of UI protection.
5. Automation
Since clickjacking requires human interaction, we cannot automate the attack itself simply. However, we can write a Python tool to quickly scan endpoints for missing anti-framing headers.
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
#!/usr/bin/env python3
import argparse
import requests
import sys
def check_clickjacking(url):
print(f"[*] Scanning {url} for Clickjacking protections...")
try:
resp = requests.get(url)
headers = {k.lower(): v.lower() for k, v in resp.headers.items()}
vulnerable = True
# Check Legacy X-Frame-Options
xfo = headers.get('x-frame-options')
if xfo in ['deny', 'sameorigin']:
print(f"[+] Found X-Frame-Options: {xfo.upper()}")
vulnerable = False
else:
print("[-] Missing or weak X-Frame-Options header.")
# Check Modern Content-Security-Policy
csp = headers.get('content-security-policy', '')
if 'frame-ancestors' in csp:
print(f"[+] Found CSP frame-ancestors directive.")
vulnerable = False
else:
print("[-] Missing CSP frame-ancestors directive.")
print("-" * 40)
if vulnerable:
print("[!!!] VULNERABLE: The page can be framed. Clickjacking is possible.")
else:
print("[*] SECURE: Anti-framing headers are present.")
except requests.exceptions.RequestException as e:
print(f"[-] Network error: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Scan an endpoint for Clickjacking vulnerability (missing headers).")
parser.add_argument("url", help="Target URL (e.g., the /my-account page)")
args = parser.parse_args()
check_clickjacking(args.url)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
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).
Remove this configuration to restore the default protection, or use '.sameOrigin()'.
severity: ERROR
patterns:
- pattern: |
$HTTP. ... .headers(). ... .frameOptions().disable()
Technical Flow & Syntax Explanation:
.frameOptions().disable(): This pattern specifically hunts for the exact method chain where a developer intentionally turns off the framework’s built-in clickjacking defense.
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.
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: Targets the main ASP.NET Core configuration block where middleware is wired up.pattern-not-inside: This logic flags the entire block unless it detects known safe patterns, such as the use of a security headers library (UseSecurityHeaders) or manual header injection (Headers.Add("X-Frame-Options")).
