Insecure Direct Object Reference in Flask with Mongodb
Insecure Direct Object Reference in Flask with Mongodb — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to a resource—typically a database identifier—and relies on the client to supply which object to act upon, without verifying that the requesting user is authorized for that specific object. In a Flask application using MongoDB as the backend, IDOR commonly arises when route parameters such as :user_id or :document_id are directly used to query a collection without confirming that the authenticated subject has permission to access or modify the corresponding document.
Consider a Flask route that retrieves a user profile by ID from MongoDB using request.args.get('id') or a URL path variable. If the route does not enforce ownership or role-based checks, an authenticated user can simply change the supplied ID to access or manipulate another user's data. Because MongoDB queries often use the _id field (typically an ObjectId) or other unique fields like username or email, an attacker can iterate through valid ObjectIds or guess predictable identifiers to enumerate records.
For example, suppose the application uses a URL pattern like /api/profile?user_id=65a1b2c3d4e5f6a7b8c9d0e1. If the server constructs a MongoDB query such as db.users.find_one({'_id': ObjectId(user_id)}) without confirming that the authenticated session's user matches the requested user_id, this is a classic IDOR. The unauthenticated attack surface of Flask apps with MongoDB is often larger than expected when endpoints inadvertently expose ObjectId values in URLs or error messages, and when ObjectId handling is inconsistent between the application and the database.
Additionally, IDOR can intersect with other checks that middleBrick runs in parallel, such as Authentication and Property Authorization. Even when authentication succeeds, insufficient property-level authorization allows attackers to exploit weak scoping in queries. For instance, a route that updates a document by ID but fails to scope the update to the user's tenant or role can enable privilege escalation across tenants. The risk is compounded when the OpenAPI spec defines path parameters without clarifying authorization requirements, because runtime findings may reveal discrepancies between declared and actual access controls.
Real-world attack patterns mirror this: an authenticated user modifies another user's settings by changing an integer or ObjectId in the request, or an attacker uses tools to enumerate IDs sequentially. Because MongoDB can return different error messages depending on whether a document exists or the query is malformed, attackers can sometimes infer valid IDs. MiddleBrick’s checks for BOLA/IDOR, Input Validation, and Property Authorization are designed to surface these classes of issues by comparing spec definitions with observed runtime behavior, including how endpoints handle crafted identifiers.
To mitigate IDOR in Flask with MongoDB, developers must enforce strict access controls at the data-access layer, ensure that every query includes user context or tenant scoping, and validate that the requesting subject is explicitly authorized for the targeted document. Relying on security through obscurity of ObjectId values or hiding database identifiers is not sufficient. Instead, design endpoints so that the server constructs query filters that include ownership or role constraints, and always apply the principle of least privilege when defining database user permissions.
Mongodb-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring that every MongoDB query includes authorization criteria tied to the authenticated subject. Below are concrete, Flask-compatible patterns using PyMongo that demonstrate secure handling of user-specific data.
1. Always include user ownership in the query filter
Instead of fetching a document solely by client-provided ID, combine the ID with the authenticated user's identifier. This prevents users from accessing other users' documents simply by changing an ID parameter.
from flask import request, jsonify
from pymongo import MongoClient
from bson.objectid import ObjectId
client = MongoClient('mongodb://localhost:27017')
db = client['mydb']
users = db['users']
@app.route('/api/profile', methods=['GET'])
def get_profile():
# Assume current_user is derived from session or token
current_user = getattr(g, 'current_user', None)
if not current_user:
return jsonify({'error': 'unauthorized'}), 401
# Use both the requested ID and the authenticated user's ID
requested_id = request.args.get('id')
if not requested_id:
return jsonify({'error': 'missing id'}), 400
# Ensure the requested document belongs to the current user
profile = users.find_one({'_id': ObjectId(requested_id), 'user_id': current_user['user_id']})
if profile is None:
return jsonify({'error': 'not found or insufficient permissions'}), 404
return jsonify(dict(profile))
2. Scope updates and deletes with ownership and role checks
When modifying or deleting documents, embed the user context into the filter so that users cannot escalate privileges by targeting other users' records.
@app.route('/api/profile', methods=['PUT'])
def update_profile():
current_user = getattr(g, 'current_user', None)
if not current_user:
return jsonify({'error': 'unauthorized'}), 401
data = request.get_json()
requested_id = data.get('id')
if not requested_id:
return jsonify({'error': 'missing id'}), 400
# Update only if the document belongs to the current user
result = users.update_one(
{'_id': ObjectId(requested_id), 'user_id': current_user['user_id']},
{'$set': data}
)
if result.matched_count == 0:
return jsonify({'error': 'not found or insufficient permissions'}), 404
return jsonify({'status': 'updated'})
3. Avoid exposing internal ObjectIds in APIs; use opaque identifiers when possible
While not always practical, using a secondary, non-sequential identifier can reduce the risk of ID enumeration. When using ObjectId, ensure that lookups always include user context.
@app.route('/api/shared-docs', methods=['GET'])
def list_shared_docs():
current_user = getattr(g, 'current_user', None)
if not current_user:
return jsonify({'error': 'unauthorized'}), 401
# Fetch documents shared with the current user via an access list
docs = list(db['documents'].find(
{'shared_with': current_user['user_id']},
{'_id': 0, 'doc_id': 1, 'title': 1}
))
return jsonify(docs)
4. Validate and sanitize input to prevent NoSQL injection
Always validate identifiers on the server side and avoid directly interpolating user input into queries. Use PyMongo’s ObjectId conversion carefully and handle exceptions.
from werkzeug.exceptions import BadRequest
def safe_get_object_id(id_str):
try:
return ObjectId(id_str)
except Exception:
raise BadRequest('invalid object id')
By combining these patterns—mandatory user scoping, strict validation, and avoiding over-reliance on ID secrecy—you significantly reduce the IDOR attack surface for Flask applications backed by MongoDB. MiddleBrick’s checks for BOLA/IDOR, Input Validation, and Property Authorization help confirm that these controls are correctly implemented in practice.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |