Cross Site Request Forgery in Flask with Firestore
Cross Site Request Forgery in Flask with Firestore — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) in a Flask application that uses Cloud Firestore arises when state-changing requests rely solely on cookies for authentication without anti-CSRF tokens. Firestore security rules typically allow access based on Firebase Authentication UID; if a browser automatically includes credentials (cookies or session identifiers) with each request to your Flask backend, an attacker can craft malicious pages that cause the browser to issue unintended writes or deletes to Firestore via your Flask endpoints.
Consider a Flask route that updates a Firestore document based on user-supplied input without verifying intent:
from flask import request, jsonify
import firebase_admin
from firebase_admin import credentials, firestore
cred = credentials.Certificate('service-account.json')
firebase_admin.initialize_app(cred)
db = firestore.client()
@app.route('/update_profile', methods=['POST'])
def update_profile():
user_id = request.cookies.get('user_id')
data = request.get_json()
if not user_id or not data:
return jsonify({'error': 'missing data'}), 400
# Vulnerable: no CSRF protection, no origin/referer check
doc_ref = db.collection('users').document(user_id)
doc_ref.update({
'display_name': data.get('display_name', ''),
'email': data.get('email', '')
})
return jsonify({'status': 'ok'})
In this example, an authenticated user’s browser sends the session cookie to /update_profile. An attacker can trick the user into submitting a POST from an external site (e.g., an image tag or form submission) that modifies the user’s Firestore document. Because Firestore rules validate UID ownership but not request origin, the update succeeds from the attacker’s forged request.
The risk is compounded when Firestore rules are permissive for authenticated users (e.g., allowing users to write to their own document path). Combined with missing anti-CSRF controls in Flask, this enables unauthorized operations such as changing email, escalating roles stored in Firestore, or creating unintended documents.
CSRF against Firestore endpoints is notable because Firestore tokens or ID tokens are often stored in cookies or local storage. If your Flask app uses session cookies without SameSite and Secure flags, or if JavaScript on the frontend attaches ID tokens via headers in a way that persists across cross-origin requests, the attack surface expands. The scanner checks for missing CSRF protections and unsafe exposure of write endpoints that rely only on cookie-based auth without origin validation.
Firestore-Specific Remediation in Flask — concrete code fixes
Remediate CSRF in Flask with Firestore by ensuring each state-changing request includes an anti-CSRF token and validating origin/referer headers. Do not rely on cookies alone for write operations. Below are concrete, Firestore-aware examples.
1) Generate and validate synchronizer token pattern (recommended):
from flask import session, request, jsonify, g
import firebase_admin
from firebase_admin import credentials, firestore
import secrets
cred = credentials.Certificate('service-account.json')
firebase_admin.initialize_app(cred)
db = firestore.client()
@app.before_request
def ensure_csrf_token():
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
token = session.get('csrf_token')
provided = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
if not token or not secrets.compare_digest(token, provided):
return jsonify({'error': 'invalid csrf token'}), 403
@app.route('/update_profile', methods=['POST'])
def update_profile():
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'unauthenticated'}), 401
data = request.get_json()
if not data:
return jsonify({'error': 'missing data'}), 400
# Validate origin for high-risk operations
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
if not origin or not referer or 'yourdomain.com' not in origin:
return jsonify({'error': 'invalid origin'}), 403
doc_ref = db.collection('users').document(user_id)
doc_ref.update({
'display_name': data.get('display_name', ''),
'email': data.get('email', '')
})
return jsonify({'status': 'ok'})
2) Use Flask-WTF or a lightweight CSRF helper to set and verify tokens per session, and ensure cookies have SameSite=Lax or Strict where appropriate:
from flask import Flask, session, request, jsonify
from flask_wtf.csrf import CSRFProtect, generate_csrf
app = Flask(__name__)
app.config['SECRET_KEY'] = 'change-this-to-a-strong-secret'
csrf = CSRFProtect(app)
@app.route('/api/token')
def get_csrf_token():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return jsonify({'csrf_token': session['csrf_token']})
@app.route('/firestore_update', methods=['POST'])
def firestore_update():
csrf.protect() # raises 400 if token mismatch
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'unauthenticated'}), 401
# Proceed with Firestore update as above
3) Tighten Firestore security rules to require not just ownership but also additional signals for sensitive operations. Combine with custom claims for role checks and validate incoming data in Flask before writing:
# Example Firestore rule (conceptual; not executed here)
# rules_version = '2';
# service cloud.firestore {
# match /databases/{database}/documents {
# match /users/{userId} {
# allow read, write: if request.auth != null && request.auth.uid == userId
# && request.time - request.auth.token.creation_time < duration.value(15, 'm');
# }
# }
# }
4) Enforce SameSite and Secure cookie attributes for session cookies in Flask:
from flask import Flask, session
app = Flask(__name__)
app.config.update(
SESSION_COOKIE_SAMESITE='Lax',
SESSION_COOKIE_SECURE=True,
REMEMBER_COOKIE_SAMESITE='Lax',
REMEMBER_COOKIE_SECURE=True
)
These changes ensure that each write to Firestore initiated by Flask is backed by an explicit token, validated origin, and secure cookie settings, significantly reducing CSRF risk while keeping Firestore UID-based rules as a secondary layer.