Use After Free in Flask with Bearer Tokens
Use After Free in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Use After Free (UAF) in a Flask API that uses Bearer Tokens occurs when token-related resources are released or invalidated on the server but references to that memory persist in logic or data structures, and later requests reuse those stale references. Although Python’s runtime includes automatic garbage collection, UAF-like conditions can still manifest through patterns such as caching token state in mutable globals, storing decoded payloads in request-bound structures that are reused across invocations, or failing to fully clear token material after logout or revocation.
When Bearer Tokens are involved, the typical flow is: client sends an Authorization header, Flask decodes or validates the token (often via a library such as PyJWT), attaches claims to g or another request-scoped holder, and uses those claims for authorization. If the implementation caches decoded token data in a global dictionary keyed by token identifier (e.g., jti) and does not reliably remove entries on logout or token expiry, a later request with a different token that reuses the same identifier can observe the previous token’s claims.
In a black-box scan, middleBrick tests unauthenticated attack surface paths and checks for inconsistent authorization boundaries. It can detect scenarios where authorization checks rely on stale token metadata, or where token revocation is not reflected in runtime decisions. For example, if an endpoint reads g.user_permissions that were populated from an earlier token, and the token has since been revoked, the API may erroneously allow an action that should be denied. This maps to authorization flaws such as BOLA/IDOR when the confused identity is derived from token handling rather than direct resource ownership.
Consider a Flask route that caches decoded payloads in a module-level dictionary without cleanup:
from flask import Flask, request, g
import jwt
app = Flask(__name__)
CACHE = {}
@app.before_request
def load_token_state():
auth = request.headers.get('Authorization', '')
if auth.startswith('Bearer '):
token = auth.split(' ')[1]
# Decode without verifying signature in a misconfigured example
payload = jwt.decode(token, options={'verify_signature': False})
# Store by jti — if not cleaned on logout, UAF-like reuse can occur
if 'jti' in payload:
if payload['jti'] not in CACHE:
CACHE[payload['jti']] = payload
g.token_payload = CACHE.get(payload['jti'])
else:
g.token_payload = None
@app.route('/admin')
def admin():
if g.token_payload and g.token_payload.get('role') == 'admin':
return 'admin access granted'
return 'forbidden', 403
If the application later removes entries from CACHE inconsistently (e.g., on logout for one token family but not another), or if the jti space is small and collisions occur, a request with a different token can obtain the cached claims of a previously freed token. middleBrick’s checks for Property Authorization and BOLA/IDOR would flag such routes when authorization depends on mutable global state rather than current request validation.
Another scenario involves token introspection endpoints storing transient state that is not properly cleared. Suppose Flask caches introspection results to avoid repeated validation, and the cache eviction policy is weak or keyed only by token string. If a token is rotated or revoked, stale entries may be served, causing authorization decisions based on outdated permissions — a UAF-like condition in the logic layer.
These issues are not about memory safety in the C sense, but about logical reuse of authorization state. middleBrick’s unauthenticated scan surface testing can surface these by checking whether endpoints behave differently when tokens are revoked, expired, or replaced while cached references remain. Remediation focuses on ensuring token-bound data is tied strictly to the request lifecycle and is purged on logout, expiry, or revocation, and by avoiding global caches for authorization state.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
Remediation centers on strict request-scoped validation, avoiding persistent caches for authorization claims, and ensuring token revocation is respected before any authorization decision. The following patterns demonstrate secure handling of Bearer Tokens in Flask.
1. Validate on each request and avoid global caching
Decode and verify the token on every request, and do not store decoded payloads in mutable globals. Use flask.g only for the current request:
from flask import Flask, request, g
import jwt
from jwt.exceptions import InvalidTokenError
app = Flask(__name__)
SECRET_KEY = 'your-secure-key'
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except InvalidTokenError:
return None
@app.before_request
def load_token_state():
auth = request.headers.get('Authorization', '')
if auth.startswith('Bearer '):
token = auth.split(' ')[1]
payload = verify_token(token)
g.token_payload = payload
else:
g.token_payload = None
@app.route('/resource')
def access_resource():
if not g.token_payload:
return 'unauthorized', 401
# Use claims directly from g.token_payload, no stale cache
return 'ok', 200
2. Enforce revocation on logout
If you maintain a denylist (e.g., for logged-out tokens until expiry), store only token identifiers with an expiration aligned to the token TTL, and clean them deterministically. A simple in-memory set with TTL can be implemented via a cache library, but ensure it is request-bound for reads and safely invalidated on logout:
from flask import Flask, request, g
import jwt
import time
from collections import OrderedDict
app = Flask(__name__)
SECRET_KEY = 'your-secure-key'
# Simple ordered denylist with TTL housekeeping
DENYLIST = OrderedDict()
DENYLIST_TTL = 3600 # seconds, should match token expiry policy
def add_to_denylist(jti: str, exp: int):
DENYLIST[jti] = exp
# Trim oldest entries to bound size
while len(DENYLIST) > 10_000:
DENYLIST.popitem(last=False)
# Expire old entries
now = time.time()
expired = [k for k, v in DENYLIST.items() if v <= now]
for k in expired:
DENYLIST.pop(k, None)
def is_denied(jti: str) -> bool:
return jti in DENYLIST
@app.route('/logout', methods=['POST'])
def logout():
auth = request.headers.get('Authorization', '')
if auth.startswith('Bearer '):
token = auth.split(' ')[1]
payload = verify_token(token)
if payload and 'jti' in payload:
# Store with expiry to bound memory
add_to_denylist(payload['jti'], payload.get('exp', int(time.time()) + 3600))
return 'logged out', 200
def verify_token_with_denylist(token: str) -> dict:
payload = verify_token(token)
if payload and 'jti' in payload and is_denied(payload['jti']):
return None
return payload
@app.before_request
def load_token_state_safe():
auth = request.headers.get('Authorization', '')
if auth.startswith('Bearer '):
token = auth.split(' ')[1]
g.token_payload = verify_token_with_denylist(token)
else:
g.token_payload = None
3. Use robust libraries and avoid custom crypto
Always use maintained libraries for decoding and verification. Do not implement your own signature verification or manipulate token headers manually. Configure audience and issuer validation to prevent token confusion across services.
These patterns ensure token state is not reused across requests and that revocation is respected. middleBrick’s GitHub Action can be added to CI/CD pipelines to fail builds if risk scores exceed your threshold, while the Web Dashboard and MCP Server help track and investigate findings interactively.