C / FEB 20, 2026
Day 7 — Cross-Site Request Forgery (CSRF) in Flask: Account Takeover via Session Riding & Proper Mitigation

By Hafiz Shamnad • 6 min read
During a hands-on web security practice series, a deliberately vulnerable Flask-based authentication system was developed to evaluate whether standard authentication mechanisms sufficiently protect sensitive actions. The analysis revealed that authentication alone is inadequate, as it enabled an attacker to perform an account takeover by changing the administrator's password without knowledge of credentials, session cookie theft, or login bypass. This vulnerability stemmed from the application's implicit trust in browser-attached session cookies, exploiting the browser's automatic inclusion of credentials in cross-origin requests.
This technical write-up details the vulnerability's root cause, exploitation methodology, impact assessment, and secure remediation strategies. It incorporates insights from established security standards such as OWASP guidelines, Flask-specific extensions like Flask-WTF, and real-world CSRF incidents to provide a comprehensive analysis suitable for security researchers, developers, and penetration testers.
Application Overview
The target application was a minimal Flask web server implementing a basic authentication system:
- Login Endpoint: Handles username and password validation, issuing a signed session cookie upon successful authentication.
- Session Management: Utilizes Flask's built-in session handling with a secret key for signing.
- Sensitive Action: Password change functionality accessible via a POST request.
Key code snippets:
- Login route:
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'secret': # Simplified for demo
session['user'] = username
return redirect('/dashboard')
return 'Invalid credentials'
- Password change endpoint (vulnerable version):
@app.route('/change_password', methods=['POST'])
def change_password():
if 'user' in session:
new_password = request.form['new_password']
# Update password in database (simulated)
return 'Password changed successfully'
return 'Unauthorized', 401
At initial inspection, the endpoint appears secure due to the session check. However, this relies solely on authentication (identity verification) without origin or intent validation, creating a classic CSRF exposure.
Root Cause Analysis
The vulnerability arises from the application's trust in session cookies as implicit proof of user intent. Web browsers adhere to the Same-Origin Policy for reading responses but not for sending requests; they automatically attach all relevant cookies (including session cookies) to requests targeting the cookie's domain, regardless of the request's origin.
- Authentication vs. Authorization Gap: The server verifies the user's identity via the session cookie but fails to confirm that the request originated from the application's own interface or was intentionally initiated by the user.
- Browser Mechanics: Upon authentication, the server sets a cookie:
Set-Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Zh123; Path=/; HttpOnly
Subsequent requests to the domain include:
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Zh123
This attachment occurs even for requests triggered from third-party sites, enabling "session riding."
CSRF exploits this by forging requests that mimic legitimate ones, leveraging the browser's credential attachment to bypass explicit authentication while compromising integrity (e.g., unauthorized state changes).
Understanding Browser Behavior in CSRF
Browsers enforce the Same-Origin Policy to protect confidentiality (preventing cross-origin reads) but allow cross-origin writes (requests) with automatic credential inclusion. This asymmetry enables CSRF:
- Credential Inclusion: Cookies are sent with requests if the domain matches, irrespective of the initiating origin.
- HTTP Method Relevance: CSRF typically targets state-changing methods like POST, but GET-based attacks are possible if endpoints allow side effects (e.g., via query parameters).
- JavaScript Triggers: Attacks can use XMLHttpRequest or Fetch API if CORS permits, but basic forms suffice for most exploits.
This behavior is not a browser flaw but a web standard (RFC 6265) that shifts responsibility to applications for request validation.
Attack Construction and Vectors
A proof-of-concept exploit was crafted using a malicious HTML page (evil.html) hosted on a separate origin:
<!DOCTYPE html>
<html>
<body>
<form action="http://127.0.0.1:5000/change_password" method="POST" id="csrf-form">
<input type="hidden" name="new_password" value="hacked123">
</form>
<script>
document.getElementById("csrf-form").submit();
</script>
</body>
</html>
- Automation: JavaScript auto-submits the form upon page load, requiring no user interaction.
-
Variations:
-
GET-based: If the endpoint supported GET (e.g.,
/change_password?new_password=hacked123), an<img src="...">tag could trigger it silently. - Stored CSRF: Payload embedded in victim site's user-generated content (e.g., via XSS synergy).
- Login CSRF: Forces login to an attacker-controlled account, capturing data post-authentication.
-
GET-based: If the endpoint supported GET (e.g.,
Exploitation Steps
- Victim authenticates to the Flask app, receiving a valid session cookie.
- Attacker lures victim to malicious page (e.g., via phishing email or compromised site).
- Browser loads evil.html and executes the script.
- Form submits POST to target endpoint with auto-attached cookie.
- Server processes as legitimate due to valid session.
Observed request:
POST /change_password HTTP/1.1
Host: 127.0.0.1:5000
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Zh123
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
new_password=hacked123
The server lacks mechanisms to distinguish this from a user-initiated request.
Why the Attack Succeeds
- No Origin Validation: Absence of checks like CSRF tokens allows forged requests.
- Browser Trust Exploitation: Automatic cookie attachment bypasses need for credential theft.
- Integrity Violation: Confidentiality (e.g., response reading) is protected by SOP, but actions altering state are not.
Impact Assessment
In this demo, the attack enabled full account takeover. Broader implications include:
- Privilege Escalation: Abuse of admin actions.
- Financial Loss: Unauthorized transfers (e.g., banking apps).
- Data Manipulation: Email/password changes, configuration alterations.
-
Real-World Examples:
- uTorrent (2008): CSRF via GET requests downloaded malware on a mass scale (CVE-2008-6586).
- TikTok (2020): Allowed one-click account takeovers via password reset endpoints.
- ING Direct (2008): Enabled unauthorized fund transfers.
- Tesla (2017): Remote vehicle command execution via web interface.
No need for MITM, password cracking, or client compromise; only a logged-in victim and social engineering.
Mitigation Strategies
Synchronizer Token Pattern (Primary Defense)
Implemented using Flask-WTF for automatic token generation and validation.
- Setup:
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
csrf = CSRFProtect(app)
app.secret_key = 'strong_secret_key'
class PasswordForm(FlaskForm):
new_password = StringField('New Password', validators=[DataRequired()])
submit = SubmitField('Change Password')
- Protected route:
@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
if 'user' not in session:
return 'Unauthorized', 401
form = PasswordForm()
if form.validate_on_submit():
# Update password
return 'Password changed'
return render_template('change_password.html', form=form)
- Template:
<form method="POST">
{{ form.hidden_tag() }} <!-- Injects CSRF token -->
{{ form.new_password.label }} {{ form.new_password }}
{{ form.submit }}
</form>
Tokens are 256-bit random values, bound to sessions, and validated on submission.
Why the Attack Fails Post-Mitigation
Attackers cannot obtain the token due to SOP preventing cross-origin reads. Forged requests lack the token, triggering rejection (e.g., 403 Forbidden).
Advanced Mitigations
- Double Submit Cookie: Store token in cookie and require matching in form/header. Simpler but less secure than synchronizer if not randomized per-request.
-
SameSite Cookies: Set
SameSite=LaxorStrictto restrict cross-site cookie sending.
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
-
Custom Request Headers: Require non-standard headers (e.g.,
X-Requested-With: XMLHttpRequest) for AJAX, blocked by CORS preflights. - User Interaction Requirements: CAPTCHA or re-authentication for high-risk actions.
- Token Enhancements: Per-action tokens, expiration (e.g., 30 minutes), rotation post-use, constant-time comparison to prevent timing attacks.
Additional Security Hardening
-
Password Storage: Use Werkzeug's
generate_password_hashwith PBKDF2, SHA256, and high iterations. - Session Fixation Prevention: Regenerate session ID on login:
session.regenerate()
-
Cookie Attributes:
HttpOnly,Secure,SameSite=Lax. - Rate Limiting: Throttle sensitive endpoints to mitigate brute-force or repeated exploits.
- Framework Defaults: Modern Flask extensions like Flask-WTF provide built-in CSRF for forms; extend to AJAX via manual token handling.
Testing and Tools
- Manual Testing: Use Burp Suite to replay requests without tokens; craft PoCs with CSRF PoC Creator extension.
- Automated Tools: XSRFProbe for scanning, Bolt for exploitation automation.
- Bypass Considerations: Test for header validation flaws (e.g., missing Referer checks) or token leaks via XSS.
Security Lessons
CSRF underscores that authentication verifies "who," but not "what" or "why." Secure logins can coexist with vulnerable actions if intent isn't validated. This is why OWASP ranks CSRF in its Top 10 (historically A5: Broken Access Control) and recommends multi-layered defenses.
The vulnerability lies not in authentication but in unchecked browser trust.
Conclusion
This exercise illustrated a stealthy attack requiring minimal prerequisites: an authenticated victim and a malicious lure. CSRF persists due to inherent web mechanics, but mitigations like tokens and SameSite attributes render it ineffective. Frameworks like Flask, with extensions, simplify protection—yet custom implementations demand rigorous testing. In production, combine defenses and monitor for emerging bypasses to safeguard post-authentication integrity.



