Cache Poisoning in Flask with Bearer Tokens
Cache Poisoning in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Cache poisoning in Flask applications that rely on Bearer tokens can occur when responses containing sensitive authorization data are inadvertently stored and served to other users. This typically happens when caching logic does not take into account the per-user nature of tokens, leading to one user’s token being cached and reused for another. When a Flask route uses token-based authentication but generates cache keys that ignore the Authorization header, two distinct risks emerge.
First, consider a route that caches the full HTTP response based only on the request path and query parameters. If the route serves both public and authenticated content and does not differentiate by the Bearer token, an authenticated user’s response might be stored under a key that another user can later retrieve. For example, a route that returns user profile information may cache the JSON body without including the token in the cache key. An attacker who can observe or predict URLs might leverage this to receive another user’s data, which may include sensitive details embedded in the response body or headers.
Second, token leakage can happen at the application or infrastructure level when cached responses include Authorization headers or tokens in URLs. If a Flask app logs responses or exposes debug information, cached entries that contain Authorization headers might be written to logs or error traces. In addition, reverse proxies or CDNs that cache based on full URLs might store responses that include tokens as query parameters, especially if the app appends the token to the URL instead of using the Authorization header properly. Such misconfigurations violate the principle that Bearer tokens should remain opaque and not appear in logs, URLs, or cached representations accessible to other users.
Cache poisoning in this context is closely related to the broader category of Insecure Direct Object References (IDOR) and Broken Access Control, because the vulnerability allows one user to access another user’s cached data. Common OWASP API Top 10 categories such as Broken Object Level Authorization and Security Misconfiguration apply here. Real-world patterns seen in SSRF and input validation issues can further amplify the risk if user-supplied data influences cache keys without proper validation or normalization. A thorough scan using tools that test unauthenticated attack surfaces and inspect OpenAPI specifications can identify routes where cache behavior does not account for authorization headers.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
To remediate cache poisoning risks when using Bearer tokens in Flask, ensure that cached responses are keyed by the full set of authorization context and that tokens never appear in logs, URLs, or cached payloads. Below are concrete, secure patterns you can adopt.
1. Include the token or its derived scope in the cache key
When caching authenticated responses, incorporate the Authorization header or a canonical representation of it into the cache key. This prevents one user’s cached response from being served to another.
from flask import request, jsonify
import hashlib
def get_cache_key():
path = request.path
method = request.method
auth = request.headers.get('Authorization', '')
# Create a deterministic key that includes the Authorization header
key_source = f'{method}:{path}:{auth}'
return 'cache:' + hashlib.sha256(key_source.encode()).hexdigest()
@app.route('/api/profile')
def profile():
cache_key = get_cache_key()
cached = cache.get(cache_key)
if cached is not None:
return cached
# Perform authenticated logic using the Bearer token
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_data = get_user_data_for_token(token)
resp = jsonify(user_data)
cache.set(cache_key, resp, timeout=300)
return resp
2. Avoid exposing tokens in URLs and logs
Always transmit Bearer tokens in the Authorization header and never append them as query parameters. Ensure that Flask’s logging configuration does not capture headers containing Authorization.
import logging
from flask import Flask
app = Flask(__name__)
class FilterSensitiveHeaders(logging.Filter):
def filter(self, record):
# Prevent Authorization headers from appearing in logs
if hasattr(record, 'msg'):
record.msg = str(record.msg).replace(request.headers.get('Authorization', ''), '[REDACTED]')
return True
handler = logging.StreamHandler()
handler.addFilter(FilterSensitiveHeaders())
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
@app.before_request
def redact_auth_from_logs():
# Example of safe handling: do not log Authorization headers
app.logger.info('Request received', extra={'headers': {k: v for k, v in request.headers if k.lower() != 'authorization'}})
3. Validate and normalize inputs that influence caching
Ensure that any user-controlled data used to construct cache keys is validated and normalized to prevent cache key confusion attacks. Reject malformed tokens early and avoid using raw user input in cache logic.
from werkzeug.exceptions import BadRequest
def validate_token_format(token: str) -> bool:
# Basic validation: non-empty, correct scheme, no suspicious characters
if not token or ' ' in token or len(token) > 4096:
return False
return token.startswith('Bearer ') and all(c.isalnum() or c in '._-~' for c in token[7:])
@app.before_request
def ensure_valid_auth():
if request.endpoint and requires_auth(request.endpoint):
auth = request.headers.get('Authorization', '')
if not validate_token_format(auth):
raise BadRequest('Invalid Authorization header')
4. Use framework-level cache controls for authenticated routes
Configure HTTP cache headers carefully so that shared caches do not store authenticated responses. Explicitly set Vary and prevent storage of sensitive responses.
from flask import make_response
@app.route('/api/data')
def get_data():
resp = make_response(jsonify(get_protected_data()))
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private'
resp.headers['Vary'] = 'Authorization'
return resp