CSRF & Clickjacking

Understand and exploit Cross-Site Request Forgery and Clickjacking — two attacks that abuse browser trust — and implement robust defenses for each.

Medium 55m 3 tasks

Learning Objectives

  • Explain how CSRF exploits cookie-based authentication
  • Craft CSRF PoC forms for GET and POST requests
  • Implement CSRF tokens, SameSite cookies, and double-submit cookie patterns
  • Exploit clickjacking to hijack button clicks via iframes
  • Implement frame-busting and X-Frame-Options defenses

Cross-Site Request Forgery (CSRF)

How CSRF Works

CSRF forces a logged-in victim's browser to send an unintended request:

1. Alice logs into bank.com  session cookie stored
2. Alice visits evil.com (still logged in)
3. evil.com's page auto-submits a form to bank.com/transfer
4. Browser includes Alice's session cookie  bank processes transfer as Alice

The bank sees a valid session cookie — it can't distinguish:
- Alice intentionally transferring $1000
- Alice's browser auto-submitting the form from evil.com

Basic CSRF PoC (GET)

<!-- On evil.com — transfers $1000 from victim's account -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">

<!-- GET with image: browser loads image URL with cookies attached -->
<!-- Works if transfer is done via GET request (very bad practice) -->

CSRF PoC (POST)

<!-- Auto-submitting form on attacker's page -->
<html>
<body onload="document.getElementById('form').submit()">
<form id="form" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="1000">
</form>
</body>
</html>

CSRF with JSON Body

JSON content type triggers CORS preflight — harder to CSRF:

// This won't work cross-origin without CORS permission:
fetch('https://bank.com/transfer', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({to: 'attacker', amount: 1000})
})

// BUT — if the endpoint also accepts text/plain:
fetch('https://bank.com/transfer', {
    method: 'POST',
    headers: {'Content-Type': 'text/plain'},  // no preflight!
    body: '{"to":"attacker","amount":1000}'    // JSON body
})

Bypassing CSRF Token Validation

Common mistakes:
1. Token not validated on server  just send any string
2. Token validated only for POST, not PUT/PATCH/DELETE
3. Token in URL (leaks via Referer header)
4. Token predictable: sequential numbers, timestamp-based
5. Token not tied to session  reuse someone else's token
6. CSRF token check skipped when method is GET

# Test: remove the CSRF token entirely → if request succeeds, broken
# Test: use a valid token from your account on victim's action → if works, not session-bound

CSRF Defense

1. CSRF Tokens (Synchronizer Token Pattern)

# Django — built-in CSRF middleware (enabled by default)
# In views:
from django.views.decorators.csrf import csrf_protect, csrf_exempt

@csrf_protect  # explicitly add protection
def transfer(request):
    if request.method == 'POST':
        # Django verifies X-CSRFToken header or csrfmiddlewaretoken POST field
        process_transfer()

# In templates:
<form method="POST">
    {% csrf_token %}  <!-- generates: <input name="csrfmiddlewaretoken" value="TOKEN"> -->
    ...
</form>

# For AJAX:
headers: {'X-CSRFToken': getCookie('csrftoken')}

2. SameSite Cookie Attribute (Best Defense)

Set-Cookie: session=abc; SameSite=Strict; HttpOnly; Secure

SameSite=Strict: Cookie NOT sent on ANY cross-site request
  → CSRF impossible (evil.com request has no session cookie)

SameSite=Lax: Cookie sent on GET top-level navigation but NOT on:
  → POST forms from other origins (form CSRF blocked)
  → sub-requests (img, iframe, fetch from other origins)
  ← Link clicks from other sites still send cookie

SameSite=None: Cookie sent everywhere (requires Secure)
  → CSRF possible — need CSRF token

3. Double Submit Cookie

# Set CSRF cookie
response.set_cookie('csrf', random_token, samesite='Strict')

# Client includes in both cookie and POST body (or header)
POST /transfer
Cookie: session=abc; csrf=TOKEN123
Body: csrf_token=TOKEN123&amount=1000

# Server verifies: csrf cookie == csrf body value
# Attacker can't read the cookie from another origin → can't forge match

4. Referer/Origin Header Validation

def validate_origin(request):
    origin = request.headers.get('Origin') or request.headers.get('Referer', '')
    if not origin.startswith('https://bank.com'):
        return HttpResponseForbidden('Invalid origin')
# Note: some proxies strip Referer — fallback needed

Clickjacking

How Clickjacking Works

Attacker overlays an invisible iframe over a fake button:

<!-- Attacker's page -->
<style>
iframe {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    opacity: 0;        /* invisible! */
    z-index: 999;      /* on top of fake button */
}
.fake-button {
    position: absolute; top: 200px; left: 300px;
}
</style>

<div class="fake-button">Click here for a prize!</div>
<iframe src="https://bank.com/confirm-transfer?to=attacker&amount=1000"></iframe>

Victim clicks the "prize" button → actually clicks "Confirm Transfer" in the invisible iframe.

Dragjacking & Likejacking

Dragjacking: Drag-and-drop of file onto attacker page  actually drags file into iframe upload form

Likejacking: Invisible Facebook Like button overlaid on fake content
    victim "likes" attacker page without knowing

Clickjacking Defense

1. X-Frame-Options Header

X-Frame-Options: DENY                    # Never allow iframing
X-Frame-Options: SAMEORIGIN             # Only same-origin iframes
X-Frame-Options: ALLOW-FROM https://...  # Deprecated — poor browser support

2. CSP frame-ancestors (Recommended)

Content-Security-Policy: frame-ancestors 'none'        # same as DENY
Content-Security-Policy: frame-ancestors 'self'        # same origin only
Content-Security-Policy: frame-ancestors 'self' https://trusted.com

Difference: frame-ancestors in CSP is more powerful — works in more browsers.

3. JavaScript Frame-Busting (Fragile)

// Break out of iframe
if (top !== self) {
    top.location = self.location;
}

// BUT: easily bypassed with sandbox attribute:
<iframe sandbox="allow-forms allow-scripts" src="victim.com">
// sandbox prevents top.location reassignment!

// Robust version:
if (top !== self) {
    document.body.style.display = 'none';
    top.location.replace(self.location.href);
}
// Still fragile — use X-Frame-Options/CSP instead

Django: Full CSRF + Clickjacking Protection

# settings.py — enabled by default:
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',       # HSTS, HTTPS
    'django.middleware.clickjacking.XFrameOptionsMiddleware',  # X-Frame-Options
    'django.middleware.csrf.CsrfViewMiddleware',          # CSRF
    ...
]

X_FRAME_OPTIONS = 'DENY'   # or 'SAMEORIGIN'

# SecurityMiddleware also sets:
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True  # legacy — CSP is better
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

Create a Flask app with a /transfer endpoint (no CSRF protection), then: (1) build an attacker HTML page with auto-submitting form to /transfer, (2) test it with a logged-in browser, (3) add Django/Flask CSRF token protection, (4) test that the PoC now fails, (5) verify SameSite=Strict on the session cookie blocks the attack even without a CSRF token.

✦ Answer the questions to complete this task

Why does SameSite=Strict prevent CSRF?

What is the double-submit cookie pattern?

Using a local Flask app with a /admin/delete-account endpoint: (1) create an attacker page with an invisible iframe over a 'Click here to win' button, (2) set iframe opacity to 0 and position over the button, (3) demonstrate that clicking the 'win' button triggers /admin/delete-account, (4) add X-Frame-Options: DENY and verify the iframe is blocked, (5) replace with CSP frame-ancestors 'none' and test.

✦ Answer the questions to complete this task

What iframe attribute prevents frame-busting JS from working?

Test a web app for CSRF token validation weaknesses: (1) remove the CSRF token entirely — does it work? (2) use an empty string as the token, (3) use another user's valid token, (4) change request method from POST to GET (some frameworks only check POST), (5) try PUT/PATCH/DELETE methods, (6) document each finding and the correct fix.

✦ Answer the questions to complete this task

Why must CSRF tokens be tied to the user's session?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

CSRF & Clickjacking MCQ

CSRF & Clickjacking MCQ

Start →
⚙️ Practical Medium +30 XP

Secure a Vulnerable Transfer App

Secure a Vulnerable Transfer App

Start →
🚩 Challenge Hard +50 XP

CSRF Token Bypass

CSRF Token Bypass

Start →