Bola Idor in Flask with Bearer Tokens
Bola Idor in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API fails to enforce proper ownership checks between a subject (e.g., a user identified by their token) and an object they attempt to access (e.g., a resource such as /users/123). In Flask, using Bearer Tokens for authentication without adding explicit authorization checks is a common path to BOLA. A typical pattern is to validate the token and extract a subject (sub or user_id), but then directly use an incoming identifier (e.g., URL or query parameter) to fetch a database record without verifying that the record belongs to the token’s subject.
Consider a Flask route that updates a user profile using a user_id from the URL and an access token to identify the caller:
from flask import Flask, request, jsonify
import jwt
app = Flask(__name__)
SECRET = 'your-secret'
def get_current_user(token):
# naive: assumes token is valid and trusted; in practice verify signature/expiry
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return payload['sub'] # e.g., user_id as string
@app.route('/users/<user_id>', methods=['GET'])
def get_user(user_id):
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth.split(' ')[1]
subject = get_current_user(token)
# BOLA: subject and user_id are not compared against each other
user = query_user_by_id(user_id) # attacker can supply any user_id
return jsonify(user)
In this example, the token is used only for authentication (who you are), but the endpoint trusts the user_id in the path for authorization (what you are allowed to access). An attacker who obtains any valid Bearer Token can enumerate or manipulate any user_id by altering the URL, because the server never checks whether subject equals user_id. This is BOLA at the object level: the authorization decision is missing the relationship between token subject and resource ownership.
BOLA often intersects with IDOR (Insecure Direct Object References) when predictable identifiers are used. Even with Bearer Tokens, if the token does not contain enough context (for example, tenant or organization id) and the server does not scope queries by that context, horizontal privilege escalation becomes feasible. For instance, a token belonging to a user in tenant A could be used to access resources in tenant B if the endpoint does not filter by tenant_id. Stateless Bearer Tokens (JWTs) can carry scopes or roles, but those claims still must be validated against the object being requested; otherwise the token’s permissions do not prevent BOLA.
Additional risk patterns include endpoints that accept an object identifier in the body or headers instead of the URL, where developers mistakenly believe that authentication alone is sufficient. For example:
@app.route('/messages/<message_id>', methods=['DELETE'])
def delete_message(message_id):
auth = request.headers.get('Authorization')
token = auth.split(' ')[1] if auth else None
subject = get_current_user(token) if token else None
# BOLA: message_id is not checked against subject or ownership
delete_message_by_id(message_id)
return '', 204
Here the subject extracted from the token is unused for authorization, making the endpoint vulnerable to BOLA via predictable message_id values. Even with Bearer Tokens, without explicit checks the API grants access based solely on authentication, not on whether the subject owns the message_id.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring that for every request, the subject derived from the Bearer Token is explicitly compared against the object identifier before any data access. Avoid relying on token validation alone; couple authentication with per-request authorization checks.
1) Enforce subject-to-identifier equality in route handlers:
from flask import Flask, request, jsonify
import jwt
app = Flask(__name__)
SECRET = 'your-secret'
def get_current_user(token):
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return payload['sub']
def query_user_by_id(uid):
# placeholder: return dict or None
return {'id': uid, 'name': 'alice'}
@app.route('/users/<user_id>', methods=['GET'])
def get_user(user_id):
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth.split(' ')[1]
subject = get_current_user(token)
# Authorization: ensure subject owns the user_id
if subject != user_id:
return jsonify({'error': 'Forbidden'}), 403
user = query_user_by_id(user_id)
if user is None:
return jsonify({'error': 'Not found'}), 404
return jsonify(user)
This pattern decodes the Bearer Token, extracts the subject, and explicitly compares it to the user_id from the URL. If they do not match, a 403 is returned, preventing horizontal BOLA.
2) For relationships that involve indirect references (e.g., foreign keys), scope queries by subject rather than trusting the client-supplied identifier:
@app.route('/users/<user_id>/posts/<post_id>', methods=['GET'])
def get_post(user_id, post_id):
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth.split(' ')[1]
subject = get_current_user(token)
if subject != user_id:
return jsonify({'error': 'Forbidden'}), 403
# Fetch post ensuring it belongs to the user identified by user_id
post = query_post_by_user_and_post_id(subject, post_id)
if post is None:
return jsonify({'error': 'Not found'}), 404
return jsonify(post)
def query_post_by_user_and_post_id(user_id, post_id):
# placeholder: perform a scoped lookup in the database
return {'id': post_id, 'user_id': user_id, 'text': 'Hello'}
Here the subject is used both for authorization and to scope the database query, ensuring that even if post_id is predictable, only posts belonging to the subject are accessible.
3) When using roles or scopes encoded in the Bearer Token, still validate ownership explicitly for user-specific resources:
@app.route('/users/<user_id>', methods=['PUT'])
def update_user(user_id):
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth.split(' ')[1]
subject = get_current_user(token)
if subject != user_id:
return jsonify({'error': 'Forbidden'}), 403
data = request.get_json()
# perform update scoped to subject
success = update_user_by_id(subject, data)
if not success:
return jsonify({'error': 'Update failed'}), 400
return jsonify({'ok': True})
These examples demonstrate that Bearer Tokens in Flask must be paired with explicit, per-request checks that the authenticated subject is allowed to operate on the referenced object. Defense in depth includes using short-lived tokens, validating token signatures, enforcing HTTPS, and avoiding predictable identifiers.
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 |