HIGH broken authenticationflaskfirestore

Broken Authentication in Flask with Firestore

Broken Authentication in Flask with Firestore — how this specific combination creates or exposes the vulnerability

Broken Authentication occurs when identity management functions are implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens. In a Flask application using Google Cloud Firestore as the user store, several patterns specific to this stack can unintentionally weaken authentication security.

First, session management in Flask often relies on server-side sessions or signed cookies. If session identifiers or user IDs are stored directly in client-side cookies without integrity protection, an attacker who can steal or guess these values can impersonate other users. Firestore references (document paths or IDs) should never be exposed in URLs or tokens without additional verification, because an attacker can manipulate these references to access or escalate privileges across user boundaries (a BOLA/IDOR vector).

Second, password handling must be strict. Flask apps that use Firestore for user profiles must ensure passwords are hashed with a strong adaptive function such as bcrypt or Argon2. Storing plaintext or weakly hashed passwords in Firestore documents is a common misconfiguration. If the Firestore security rules rely only on user-supplied document IDs without server-side ownership checks, a malicious user may change another user’s document ID in requests and read or modify data.

Third, authentication endpoints must enforce rate limiting and account lockout to prevent credential stuffing or brute-force attacks. Without explicit rate limiting on login routes, attackers can probe credentials at scale. Firestore queries that filter by email must be deterministic and avoid leaking existence via timing differences; naive implementations that return different HTTP status codes or response shapes for missing users can enable enumeration.

Fourth, the use of custom claims in ID tokens must be carefully managed. If Flask sets or trusts custom claims from unverified sources or uses Firestore data to directly influence token claims without validation, attackers may escalate privileges by modifying claims or session data. Always verify the token on the backend and ensure Firestore-based authorization checks are independent of client-supplied data.

Finally, insecure direct object references occur when a Flask route uses a user-provided identifier to fetch a Firestore document without confirming that the authenticated user owns that document. For example, a route like /users/<user_id>/profile that directly uses user_id from the request without matching it to the authenticated UID allows horizontal privilege escalation. The combination of Flask’s flexible routing and Firestore’s straightforward document paths makes it easy to accidentally expose references that should be guarded.

Firestore-Specific Remediation in Flask — concrete code fixes

To secure authentication in Flask with Firestore, apply server-side checks, strong cryptography, and strict session handling. Below are concrete, defensive patterns with real Firestore code examples.

Password storage and verification

Store passwords using bcrypt. Never store plaintext or reversible hashes in Firestore.

import bcrypt
from google.cloud import firestore

db = firestore.Client()

def create_user(email: str, password: str):
    hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
    user_ref = db.collection('users').document(email)
    user_ref.set({
        'email': email,
        'password_hash': hashed.decode('utf-8'),  # store as string
        'created_at': firestore.SERVER_TIMESTAMP,
    })

def verify_user(email: str, password: str) -> bool:
    user_ref = db.collection('users').document(email)
    doc = user_ref.get()
    if not doc.exists:
        return False
    stored = doc.to_dict()['password_hash'].encode('utf-8')
    return bcrypt.checkpw(password.encode('utf-8'), stored)

Secure session and token handling

Use signed Flask sessions and avoid storing sensitive identifiers in the session payload. Always validate ownership on each request.

from flask import Flask, session, request
from google.cloud import firestore
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_bytes(32)
db = firestore.Client()

@app.route('/login', methods=['POST'])
def login():
    email = request.form.get('email')
    password = request.form.get('password')
    if not verify_user(email, password):
        return {'error': 'Invalid credentials'}, 401
    session['user_email'] = email  # minimal session data
    return {'ok': True}

@app.route('/profile')
def profile():
    user_email = session.get('user_email')
    if not user_email:
        return {'error': 'Unauthorized'}, 401
    user_ref = db.collection('users').document(user_email)
    doc = user_ref.get()
    if not doc.exists:
        return {'error': 'User not found'}, 404
    # Return only safe, non-sensitive fields
    return {k: v for k, v in doc.to_dict().items() if k != 'password_hash'}

Authorization with BOLA/IDOR protection

Always verify that the authenticated user owns the target resource. Use the authenticated UID (or email) to scope Firestore queries.

@app.route('/users/<user_id>/profile')
def get_user_profile(user_id: str):
    user_email = session.get('user_email')
    if not user_email:
        return {'error': 'Unauthorized'}, 401
    # Ensure the requested user_id matches the authenticated user
    if user_id != user_email:  # or map user_id to email safely
        return {'error': 'Forbidden'}, 403
    user_ref = db.collection('users').document(user_email)
    doc = user_ref.get()
    if not doc.exists:
        return {'error': 'Not found'}, 404
    return {k: v for k, v in doc.to_dict().items() if k != 'password_hash'}

Rate limiting and existence-oblivious responses

Apply rate limiting at the Flask level and use consistent response shapes to avoid user enumeration via timing or status code differences.

from flask_limiter import Limiter

limiter = Limiter(app=app, key_func=lambda: session.get('user_email') or request.remote_addr)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login_limited():
    # same verify logic as above; always return the same shape
    email = request.form.get('email')
    password = request.form.get('password')
    # Always perform hash check to avoid timing leaks, even if email not found
    verify_user(email, password)
    return {'ok': True}

Firestore security rules mindset

While Firestore rules are not a substitute for backend checks, design them to enforce ownership. In production, pair server-side authorization with rules that require authentication and match request.auth.uid with document data. In Flask, always perform your own ownership checks; do not rely solely on rules for authorization.

Related CWEs: authentication

CWE IDNameSeverity
CWE-287Improper Authentication CRITICAL
CWE-306Missing Authentication for Critical Function CRITICAL
CWE-307Brute Force HIGH
CWE-308Single-Factor Authentication MEDIUM
CWE-309Use of Password System for Primary Authentication MEDIUM
CWE-347Improper Verification of Cryptographic Signature HIGH
CWE-384Session Fixation HIGH
CWE-521Weak Password Requirements MEDIUM
CWE-613Insufficient Session Expiration MEDIUM
CWE-640Weak Password Recovery HIGH

Frequently Asked Questions

Why is storing document IDs directly in client-side cookies risky in Flask with Firestore?
It can lead to Broken Object Level Authorization (BOLA/IDOR): an attacker can modify the ID in the cookie to access or modify other users’ Firestore documents. Always validate ownership server-side and avoid exposing Firestore references in client-controlled locations.
How can I prevent user enumeration during login when using Firestore in Flask?
Use consistent response shapes and timing for login attempts regardless of whether the user exists. Always run the hash verification step (which takes similar time) even when the user is not found, and apply rate limiting to deter brute-force probing.