Privilege Escalation in Flask with Bearer Tokens
Privilege Escalation in Flask with Bearer Tokens
Privilege Escalation occurs when a user gains unauthorized access to resources or actions reserved for higher-privilege roles. In Flask applications using Bearer Tokens for authentication, this risk emerges from how tokens are issued, validated, and bound to authorization scopes. If token validation is incomplete or authorization checks are missing, an attacker can modify the token’s claims (for example, changing a role claim from user to admin) or reuse a token issued for a lower-privilege scope to access higher-privilege endpoints.
Flask itself does not enforce authorization; it relies on developers to implement checks after authentication. When Bearer Tokens are used, common pitfalls include: accepting tokens without verifying signatures or audiences, failing to validate scope/role claims on each request, and using global permissions instead of per-resource authorization. An attacker who obtains a valid Bearer Token (via leakage, insecure storage, or a compromised client) can attempt horizontal privilege escalation by altering token claims if the server is permissive, or vertical privilege escalation by leveraging weak route-level guards to access admin-only routes.
Consider a Flask route that reads a token from the Authorization header but only checks presence, not content:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/admin/settings")
def admin_settings():
auth = request.headers.get("Authorization")
if auth and auth.startswith("Bearer "):
# Vulnerable: no validation of token content/scopes/roles
return jsonify({"status": "admin settings"})
return jsonify({"error": "unauthorized"}), 401
In this example, any Bearer Token is accepted, enabling privilege escalation if an attacker can guess or reuse a token. Even when using a library like PyJWT to decode tokens, omitting scope/role validation leads to the same issue:
import jwt
from flask import request, jsonify
SECRET = "supersecret"
@app.route("/api/user/profile")
def user_profile():
token = request.headers.get("Authorization", "").replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
# Vulnerable: missing checks for 'scope' or 'role' claims
return jsonify(payload)
except jwt.PyJWTError:
return jsonify({"error": "invalid token"}), 401
Without validating claims such as scope or role, an attacker can present a token issued for read-only access to perform write or admin actions. Additionally, if token binding is weak (e.g., no checks on token issuer or audience), tokens issued for one service may be accepted by another, further enabling escalation. Proper defense combines strong token validation, strict scope/role enforcement, and per-resource authorization checks.
Bearer Tokens-Specific Remediation in Flask
Remediation focuses on strict token validation, scope/role enforcement, and minimizing trust in client-supplied claims. Always verify the token signature, issuer, audience, and expiration. Then enforce scope or role claims on every request, using a centralized authorization helper rather than repeating checks across routes.
Example: validate JWT Bearer tokens and enforce scope/role claims in Flask:
import jwt
from functools import wraps
from flask import request, jsonify
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get("Authorization")
if not auth or not auth.startswith("Bearer "):
return jsonify({"error": "missing token"}), 401
token = auth.split(" ")[1]
try:
payload = jwt.decode(
token,
"your-public-key-or-secret",
algorithms=["RS256"],
options={"require": ["exp", "iss", "aud", "scope"]},
issuer="https://auth.example.com/",
audience="myapi.example.com",
)
# Attach claims for downstream use
request.token_payload = payload
except jwt.ExpiredSignatureError:
return jsonify({"error": "token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "invalid token"}), 401
return f(*args, **kwargs)
return decorated
def require_scope(required_scope):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not hasattr(request, "token_payload"):
return jsonify({"error": "unauthorized"}), 401
scopes = request.token_payload.get("scope", "").split()
if required_scope not in scopes:
return jsonify({"error": "insufficient scope"}), 403
return f(*args, **kwargs)
return decorated
return decorator
@app.route("/api/admin/settings")
@token_required
@require_scope("admin:settings")
def admin_settings():
return jsonify({"status": "admin settings"})
@app.route("/api/user/profile")
@token_required
@require_scope("profile:read")
def user_profile():
return jsonify(request.token_payload)
Key practices:
- Verify signature with the appropriate key (symmetric or asymmetric).
- Validate standard claims:
iss(issuer),aud(audience),exp(expiration), andnbf(not before). - Enforce scope or role claims on each endpoint; prefer scope-based authorization for fine-grained control.
- Use centralized decorators or a policy engine to avoid duplicated checks.
- Store tokens securely on the client side and rotate signing keys regularly.
These steps reduce the likelihood of privilege escalation via Bearer Tokens by ensuring tokens are trustworthy and permissions are checked consistently.