Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Broken Function Level Authorization

Broken Function Level Authorization

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.