Memory Leak in Flask with Basic Auth
Memory Leak in Flask with Basic Auth
A memory leak in a Flask application using HTTP Basic Auth can arise when request-scoped or global objects are retained beyond their intended lifecycle. In a typical Flask route that validates credentials on every request, allocating buffers, large in-memory caches, or ORM sessions without proper cleanup can cause the process footprint to grow over time. When authentication is handled in before_request hooks or custom decorators, any data attached to g, session-like structures, or module-level variables may persist across requests if references are unintentionally held. This is especially relevant when the authentication logic loads user data or tokens into global caches to avoid repeated computation or I/O, and those caches are never pruned or invalidated.
Under sustained load, a Flask process handling Basic Auth may exhibit gradual RSS growth, increased garbage collection frequency, or eventual latency spikes as the runtime struggles to manage fragmented memory. Since the authentication path is exercised on nearly every request, the leak is often triggered early and amplified by high concurrency. Although Flask’s development server is not suitable for production, similar patterns in production WSGI containers can lead to steady-state memory inflation. The interaction with Basic Auth is notable because the credentials are decoded on each request; if the decoding step allocates large buffers or retains references (for example, by storing decoded payloads in a global list for logging or metrics), those allocations accumulate if not released.
Instrumentation such as memory profilers or continuous scans can surface this behavior as a risk finding, because unbounded growth can degrade reliability and availability. middleBrick’s runtime checks can detect patterns consistent with resource retention during authenticated request handling, prompting deeper investigation into object lifetimes and scope management.
Basic Auth-Specific Remediation in Flask
Remediation focuses on ensuring no per-request allocations are retained beyond the request lifecycle and that global structures are bounded or stateless. Use request-local contexts (g) appropriately and clear or reuse objects within the same request. Avoid caching decoded credentials in module-level variables; if caching is necessary, use an LRU cache with a strict size limit and TTL. Ensure that any buffers used for parsing Authorization headers are released promptly and that logging does not retain full payloads.
Below are concrete code examples for secure Basic Auth handling in Flask.
Example 1: Stateless per-request authentication without global retention
from flask import Flask, request, Response, g
import base64
app = Flask(__name__)
def get_user_credentials(token):
# Replace with secure lookup, e.g., hashed verification against a DB
# This is a simplified placeholder
if token == 'dXNlcjpwYXNz': # user:pass in base64
return {'username': 'user'}
return None
@app.before_request
def authenticate():
auth = request.authorization
if auth is None:
auth_b64 = request.headers.get('Authorization', '').replace('Basic ', '')
if auth_b64:
try:
decoded = base64.b64decode(auth_b64).decode('utf-8')
# Immediately use and discard; do not store decoded in globals
username, password = decoded.split(':', 1)
user = get_user_credentials(auth_b64)
if user:
g.user = user
return
except Exception:
pass
if not hasattr(g, 'user') or g.user is None:
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic'})
@app.route('/')
def index():
return f'Hello {g.user["username"]}'
Example 2: Using a bounded cache with maxsize and TTL (if caching is required)
from flask import Flask, request, g
from functools import lru_cache
import time
app = Flask(__name__)
# Bounded cache: max 128 entries; LRU eviction ensures memory does not grow unbounded
@lru_cache(maxsize=128)
def cached_user_info(token_hash, timestamp_block):
# Simulate a lightweight lookup; in practice this would verify credentials securely
return {'username': 'cached_user'}
@app.before_request
def authenticate_with_cache():
auth = request.authorization
if auth is None:
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic'})
token = auth.encode('utf-8') if isinstance(auth.encode('utf-8'), bytes) else auth.password
# Use request-scoped context to avoid cross-request contamination
g.user = cached_user_info(token, int(time.time() // 60)) # 1-minute block
@app.route('/')
def index_cache():
return f'Hello {g.user["username"]}'
- Do not store decoded credentials in global lists or dictionaries without eviction policies.
- Prefer request-local storage (g) and ensure it is not referenced after the response is sent.
- If using caches, bound them with size limits and time-based invalidation to prevent unbounded growth.