Cross Site Request Forgery in Flask with Mongodb
Cross Site Request Forgery in Flask with Mongodb — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) is an attack that tricks a logged-in user into submitting an unwanted request on a web application in which they are authenticated. When using Flask with MongoDB, the risk arises from a combination of session handling patterns, form validation gaps, and how state is stored and validated. Flask does not enforce view-level CSRF protections by default; developers must add it explicitly. If your Flask app relies on session cookies for authentication and exposes state-changing endpoints (POST, PUT, DELETE) without synchronizer token patterns or same-site cookie attributes, a malicious site can forge requests using the user’s credentials.
With MongoDB as the backend, the exposure often relates to how identity and permissions are stored and retrieved. For example, if your application stores a user’s role or permissions directly in the client-side session or in predictable JWTs without proper validation, an attacker can leverage forged requests to perform actions on behalf of the victim. Consider an endpoint like /api/users/change-role that accepts a JSON payload with a new role and a user identifier. If the server trusts the user ID in the request body instead of deriving it from the authenticated session, a CSRF attack can change another user’s role by sending a crafted form or image tag that executes a POST with the attacker’s chosen parameters.
MongoDB’s flexible schema can inadvertently facilitate CSRF when sensitive operations depend on client-supplied identifiers without verifying ownership against the authenticated user’s record. For instance, if a route deletes a document by an _id provided in the request without confirming that the authenticated user has the right to manage that document, a forged request can lead to unauthorized deletion or modification. Even with hashed passwords and secure authentication middleware, missing anti-CSRF tokens and weak same-site cookie policies create a path for exploitation.
Common Flask patterns that increase CSRF risk include: using Flask-Session or JWTs without strict validation, omitting the SameSite=Strict or SameSite=Lax attribute on session cookies, and building APIs that accept state-changing requests based solely on URL parameters or loosely validated JSON bodies. In MongoDB, storing authorization state in a way that is not verified on each request compounds these issues. For example, caching a user’s roles in a JWT without server-side revocation checks means a forged request carrying a valid token can still be treated as legitimate if the endpoint does not re-check permissions server-side.
To contextualize, real-world attack patterns such as those cataloged in the OWASP API Top 10 map directly to these gaps. A CSRF vector can lead to privilege escalation (BOLA/IDOR) when ownership checks are weak, and can intersect with Injection risks if input sanitation is inconsistent. Unlike desktop applications, browser-based sessions make CSRF a persistent concern for Flask apps that serve web frontends consuming MongoDB-backed APIs.
Mongodb-Specific Remediation in Flask — concrete code fixes
Mitigating CSRF in Flask with MongoDB requires a layered approach: enforce anti-CSRF tokens on state-changing endpoints, bind operations to the authenticated user’s identity, and harden cookie and session settings. Below are concrete, working code examples that demonstrate these practices.
1. Use anti-CSRF tokens and validate ownership on each request
Generate a per-session token and require it for any write operation. Store the token server-side (e.g., in the user’s MongoDB document or in a signed server-side session) and verify it on each state-changing request.
from flask import Flask, request, session, jsonify
import secrets
from pymongo import MongoClient
app = Flask(__name__)
app.secret_key = 'your-strong-secret-key'
client = MongoClient('mongodb://localhost:27017')
db = client['mydb']
users = db['users']
@app.before_request
def ensure_csrf_token():
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(16)
if request.headers.get('X-CSRFToken') != session['csrf_token']:
return jsonify({'error': 'invalid_csrf_token'}), 403
@app.route('/api/users/change-role', methods=['POST'])
def change_role():
user_id = session.get('user_id') # authenticated user from login
if not user_id:
return jsonify({'error': 'unauthorized'}), 401
data = request.get_json()
target_user_id = data.get('user_id')
new_role = data.get('role')
# Verify ownership and authorization server-side
current_user = users.find_one({'_id': user_id})
if not current_user or current_user.get('role') != 'admin':
return jsonify({'error': 'forbidden'}), 403
# Update only the target user document with server-side validation
result = users.update_one(
{'_id': target_user_id},
{'$set': {'role': new_role}}
)
if result.matched_count == 0:
return jsonify({'error': 'not_found'}), 404
return jsonify({'status': 'ok'})
2. Set secure cookie attributes and use server-side sessions
Ensure session cookies are HttpOnly, Secure, and SameSite=Strict or Lax. This reduces the likelihood that a browser will send cookies along with forged cross-site requests.
from flask import Flask
app = Flask(__name__)
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SECURE=True, # use True in production with HTTPS
SESSION_COOKIE_SAMESITE='Strict',
PERMANENT_SESSION_LIFETIME=1800
)
3. Avoid storing authorization in client-controlled data
Do not rely on request-supplied identifiers for ownership. Always resolve the user from the session or token and re-check permissions against MongoDB on every request.
@app.route('/api/items/', methods=['DELETE'])
def delete_item(item_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'unauthorized'}), 401
# Fetch the item and verify ownership
item = db.items.find_one({'_id': item_id})
if not item:
return jsonify({'error': 'not_found'}), 404
if item.get('user_id') != user_id:
return jsonify({'error': 'forbidden'}), 403
db.items.delete_one({'_id': item_id})
return jsonify({'status': 'deleted'})
4. Combine with framework extensions cautiously
If you use Flask-WTF or similar, ensure tokens are validated and not bypassed by custom endpoints. Treat JSON APIs and form submissions uniformly: require the CSRF token in headers for AJAX and hidden form fields for server-rendered pages.
These MongoDB-aware measures ensure that even if a CSRF payload reaches your Flask app, the server will reject it due to missing or mismatched tokens and will always re-verify identity and permissions against the authoritative database.