Here is an example of Python code that is vulnerable to broken function level authorization:
🥺 Vulnerable Code
@app.route("/admin/users/<int:user_id>/delete", methods=["POST"])
@login_required
def delete_user(user_id):
# Vulnerable: the button is only rendered for admins, but the route
# itself never checks the caller's role
User.query.filter_by(id=user_id).delete()
db.session.commit()
return redirect("/admin/users")@login_required proves who the caller is, not what they are allowed to do. Any authenticated user who reads the JavaScript bundle, guesses the path, or replays a request from a colleague's browser can delete accounts. Hiding a link in the UI is presentation, not authorization.
😎 Secure Code
Here is a version of the same code that is secured against broken function level authorization:
def require_role(*roles):
def decorator(view):
@wraps(view)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
abort(401)
if current_user.role not in roles:
current_app.logger.warning(
"authz_denied path=%s user=%s role=%s",
request.path, current_user.id, current_user.role,
)
abort(403)
return view(*args, **kwargs)
return wrapper
return decorator
@app.route("/admin/users/<int:user_id>/delete", methods=["POST"])
@require_role("admin")
def delete_user(user_id):
if user_id == current_user.id:
abort(400, "Admins cannot delete their own account")
User.query.filter_by(id=user_id).delete()
db.session.commit()
return redirect("/admin/users")Authorization is now a decorator on the route, so the check cannot be skipped by calling the endpoint directly, and every denial is logged for detection. Make deny the default: a central before-request hook that rejects any route without an explicit permission mapping catches the endpoint someone forgets to annotate next sprint. Add a test that replays each admin route with a low-privilege session and expects 403.