Time Of Check Time Of Use in Flask with Api Keys
Time Of Check Time Of Use in Flask with Api Keys — how this specific combination creates or exposes the vulnerability
Time Of Check Time Of Use (TOCTOU) occurs when the state used in a security decision changes between the check and the use of that resource. In Flask APIs that rely on API keys, this commonly happens when an application first validates the presence or scope of a key, then later uses request state or mutable server-side data to make authorization decisions. An attacker can alter the underlying resource between these two moments, bypassing intended restrictions.
Consider a Flask route that checks whether a key allows a given action on a resource, then proceeds to act on an identifier supplied by the client:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Example in-memory mapping: key -> set of allowed user_ids
key_permissions = {
"s3cr3tk3y_user1": {"user_id": 1},
"s3cr3tk3y_admin": {"user_id": 1, "user_id": 2},
}
@app.get("/users//profile")
def get_profile(user_id):
api_key = request.headers.get("X-API-Key")
if not api_key or api_key not in key_permissions:
return jsonify({"error": "forbidden"}), 403
# Check performed: key_permissions[api_key] contains allowed user_ids
if user_id not in {item["user_id"] for item in key_permissions[api_key]}:
return jsonify({"error": "unauthorized"}), 401
# TOCTOU gap: user_id could be changed on the server or via race conditions
# between the check above and the lookup below.
user_data = fetch_user_data_from_source(user_id) # hypothetical source
return jsonify(user_data)
def fetch_user_data_from_source(uid):
# Simulated lookup; in reality this might query a database or external service.
return {"user_id": uid, "name": "User"}
The check confirms that the API key is allowed to access the given user_id, but if the mapping between keys and user_ids is mutable or if the route uses an indirect reference (e.g., a session or token that can be swapped), an attacker who can modify server-side state or exploit a race condition can change which resource is associated with the key between the check and the data fetch. This is a classic TOCTOU pattern: the authorization decision is based on a snapshot of state that may no longer be valid when the action is performed.
With API keys stored or issued with broader scopes than intended, the risk is amplified. For example, if key_permissions is updated dynamically (e.g., admin keys grant access to all user_ids at lookup time), a stale check may appear valid while the actual access path diverges. In distributed setups where permissions are cached or replicated, the window for inconsistency widens, making the route vulnerable to horizontal or vertical privilege escalation despite the presence of checks.
LLM/AI Security checks in middleBrick highlight risks where endpoints expose sensitive logic or data flows that could be abused via timing or state manipulation. Even in unauthenticated scan modes, such architectural patterns are detectable when endpoints accept identifiers and defer validation without strong immutability guarantees.
Api Keys-Specific Remediation in Flask — concrete code fixes
Remediation centers on making the authorization decision atomic and tightly coupled with the action, minimizing or eliminating mutable state between check and use. Instead of performing a separate lookup of permissions followed by a later data fetch, embed the necessary authorization context into a verifiable token or signed value that cannot be altered without detection.
One approach is to issue short-lived, scoped tokens (or signed claims) that encode both the API key identity and the allowed user_id. The server then verifies the token and directly uses the embedded user_id without a second lookup. Below is an example using PyJWT with HS256:
import jwt
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = "super-secret-key" # use env/config in practice
def create_token(api_key, user_id, expires=300):
payload = {
"api_key": api_key,
"user_id": user_id,
"exp": time.time() + expires,
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@app.get("/users/profile")
def get_profile_secure():
token = request.headers.get("Authorization")
if not token or not token.startswith("Bearer "):
return jsonify({"error": "forbidden"}), 403
token = token.split(" ", 1)[1]
payload = verify_token(token)
if not payload:
return jsonify({"error": "invalid token"}), 401
# No further lookup; user_id is embedded and immutable for this request.
user_data = fetch_user_data_from_source(payload["user_id"])
return jsonify(user_data)
def fetch_user_data_from_source(uid):
return {"user_id": uid, "name": "User"}
Another pattern is to keep server-side mappings immutable for the duration of a request by retrieving and locking the relevant permissions once, then using that snapshot for all decisions within the request lifecycle. However, embedding scope into cryptographically signed tokens is generally more robust against race conditions and mutable state.
When API keys must remain bearer-style, ensure that every access path re-validates permissions immediately before the operation, and avoid caching or pre-resolving identifiers that can be swapped. middleBrick’s API Key and BOLA/IDOR checks can surface endpoints where authorization relies on mutable state or indirect references, guiding you toward designs where checks and usage are inseparable.