Race Condition in Flask with Bearer Tokens
Race Condition in Flask with Bearer Tokens — how this combination creates or exposes the vulnerability
A race condition in a Flask API that uses Bearer tokens typically arises when token validity checks and state-changing operations are not performed atomically. Consider an endpoint that first validates a Bearer token and then updates a resource such as a user’s email or a financial balance. If two requests with the same token arrive concurrently, the validation step may pass for both, but the state updates can interleave or produce an invalid final state. For example, an account balance read during validation may be stale by the time the write occurs, enabling a logic flaw where two parallel operations both proceed based on the same pre-update view.
In Flask, this can occur when token verification is implemented as a before-request handler (e.g., using @app.before_request) that sets g.user based on the Bearer token, but the endpoint logic itself does not re-validate invariants that must hold across the entire operation. Because the check and the side effect are separate steps, an attacker can trigger overlapping requests that exploit the timing window. This is a classic TOCTOU (time-of-check-time-of-use) pattern applied to authorization state expressed via Bearer tokens.
Race conditions are relevant to the BOLA/IDOR and Property Authorization checks in middleBrick’s 12 security checks. If your API exhibits timing-sensitive authorization flaws, middleBrick may flag these as high-severity findings in the Property Authorization or BOLA/IDOR categories, with remediation guidance to enforce atomicity and strict per-request validation. Note that middleBrick detects and reports these patterns during black-box scanning and maps findings to frameworks such as OWASP API Top 10 and SOC2.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
To mitigate race conditions when using Bearer tokens in Flask, ensure that authorization checks and state updates are performed in a single, atomic operation and that per-request re-validation is enforced. Below are two concrete patterns: one vulnerable pattern and one corrected pattern.
Vulnerable pattern: separate check and update
from flask import Flask, request, g
import jwt
app = Flask(__name__)
SECRET = 'your-secret'
# Vulnerable: token check and state update are separate steps
def get_current_user(token):
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return payload['sub']
@app.before_request
def authenticate():
auth = request.headers.get('Authorization')
if auth and auth.startswith('Bearer '):
token = auth.split(' ')[1]
g.user = get_current_user(token)
else:
g.user = None
@app.route('/transfer', methods=['POST'])
def transfer():
if g.user is None:
return {'error': 'unauthorized'}, 401
data = request.get_json()
# TOCTOU risk: balance read after auth check may be stale
balance = get_balance(g.user)
if balance < data['amount']:
return {'error': 'insufficient funds'}, 400
# Another request could have changed balance between read and write
update_balance(g.user, balance - data['amount'])
return {'status': 'ok'}, 200
Corrected pattern: re-validate and use atomic updates
from flask import Flask, request, g
import jwt
from threading import Lock
app = Flask(__name__)
SECRET = 'your-secret'
balance_lock = Lock() # Use a lock or DB transaction for atomicity
def get_current_user(token):
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return payload['sub']
def deduct_balance(user_id, amount):
# In production, use a database transaction with appropriate isolation
with balance_lock:
current = get_balance(user_id)
if current < amount:
raise ValueError('insufficient funds')
update_balance(user_id, current - amount)
@app.route('/transfer', methods=['POST'])
def transfer():
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return {'error': 'unauthorized'}, 401
token = auth.split(' ')[1]
user = get_current_user(token) # Re-validate on each request
data = request.get_json()
try:
deduct_balance(user, data['amount'])
except ValueError:
return {'error': 'insufficient funds'}, 400
return {'status': 'ok'}, 200
Key points in the corrected example:
- Token validation is repeated inside the endpoint rather than relying solely on a before-request hook, ensuring per-request freshness.
- State updates are protected by a lock (or, preferably, by database transactions with serializable isolation) to make the read-check-write sequence atomic.
- The error path returns clear, consistent responses without exposing internal state.
These practices reduce the likelihood of race conditions when using Bearer tokens. For ongoing monitoring, you can use the middleBrick CLI to scan your Flask endpoints from the terminal with middlebrick scan <url>, or integrate checks into CI/CD using the GitHub Action to fail builds if security scores drop. The Pro plan enables continuous monitoring so that future changes can be flagged before deployment.