This article is a part of Web API Security Champion series focused on API security vulnerabilities presented in a practical manner.
Broken Function Level Authorization
Description
Broken Function Level Authorization is an API vulnerability that occurs when an application fails to enforce appropriate authorization checks for users attempting to access specific functionalities or perform sensitive actions. This vulnerability allows attackers or malicious users to execute functions that should be restricted to certain roles or users with elevated privileges.
Broken Function Level Authorization is #5 on the OWASP TOP 10 API Security Risks list.
Impact
The impact of Broken Function Level Authorization can be severe, as it often allows unauthorized users to perform sensitive actions within the application. Depending on the functionality being exploited, this could lead to:
- Privilege escalation: Unauthorized users could gain higher privileges, potentially compromising the entire system.
- Data manipulation: Attackers might alter or delete data they should not have access to.
- Data exfiltration: Attackers may access sensitive data without a proper authorization.
As a side-effect of the succesfull attack, it may lead to a reputational or financial loss for the company.
Case Study — Damn Vulnerable RESTaurant API
Just like in the previous articles of the Web API Security Champion series, I will also use my FastAPI based open-source project — Damn Vulnerable RESTaurant API to present the vulnerability in a practical way.
Vulnerability Description
The Broken Function Level Authorization vulnerability is present in the /users/update_role
API endpoint of the Damn Vulnerable RESTaurant. This endpoint is intended to allow Chef of the restaurant to update user roles within the application.
Let’s take a look at the following implementation of the API endpoint and find potential security issue:
@router.put("/users/update_role", response_model=UserRoleUpdate)
async def update_user_role(
user: UserRoleUpdate,
current_user: Annotated[models.User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
# this method allows staff to give Employee role to other users
# Chef role is restricted
if user.role == models.UserRole.CHEF.value:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Only Chef is authorized to add Chef role!",
)
db_user = update_user(db, user.username, user)
return current_user
The above implementation doesn’t validate the role of a current_user
, so any authenticated user is able to change role of an arbitrary chosen user to “Employee” without proper authorization checks. The only restriction implemented here is not allowing for adding “Chef” role which is the highest privileged role in the application.
Broken Function Level Authorization Proof of Concept
Exploiting this vulnerability is pretty straightforward. Firstly, it’s required to authenticate to the application as a customer. It can be achieved via Swagger UI available at http://localhost:8080/docs by clicking on green “Authorize” button. It’s recommended to use a regular user account that can be created via /register
endpoint.
After a successfull authentication, Swagger UI can be used to send HTTP request to /users/update_role
API endpoint and change a role of a chosen user to “Employee”. The following screenshot presents request body:
Alternatively the following curl command can be utilised to send HTTP request:
curl -X 'PUT' \
'http://localhost:8080/users/update_role' \
-H 'accept: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZSIsImV4cCI6MTcyMTEyMDUwM30.ojPsAe39diQerzo_-2Ihzx7qwIKDLNr77CmzGQkmxtI' \
-H 'Content-Type: application/json' \
-d '{
"username": "username",
"role": "Employee"
}'
This request changes the role of a user named “Customer” to “Employee”, giving them access to functionalities reserved for employees.
Broken Function Level Authorization Fix
The vulnerability can be fixed by implementing role-based access control checks to ensure that only authorized users can modify roles. In the Damn Vulnerable RESTaurant API project, the RolesBasedAuthChecker
class can be used to enforce these checks.
The following endpoint implementation makes sure that only Chef can modify user’s roles.
@router.put("/users/update_role", response_model=UserRoleUpdate)
async def update_user_role(
user: UserRoleUpdate,
current_user: Annotated[models.User, Depends(get_current_user)],
db: Session = Depends(get_db),
auth=Depends(RolesBasedAuthChecker([UserRole.CHEF])),
):
if user.role == models.UserRole.CHEF.value:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Only Chef is authorized to add Chef role!",
)
db_user = update_user(db, user.username, user)
return current_user
Broken Function Level Authorization Recommendations
To prevent such vulnerabilities, I recommend the following:
- Deny access by default: Follow the principle of least privilege and only grant permissions necessary for specific roles.
- Ensure authorization checks are enforced at function level: Ensure authorization is checked at each function dedicated for handling API endpoint’s logic.
- Implement a chosen access control mechanism: e.g. use RBAC to define and enforce user roles and permissions within your application.
- Check authorizations at each function: Ensure that authorization checks are implemented at the function level to validate if the user is privileged to perform the action.
- Write unit tests for function-level authorization checks: Develop unit tests to cover scenarios with unauthorized access.
- Perform code reviews: Review code to identify potential security issues.
A recommended unit test to ensure that only authorized users can change roles:
def test_update_user_role_by_non_chef_fails(test_db, customer_client):
customer = User(
username="customer",
password="password",
first_name="Customer",
last_name="",
phone_number="12345678",
role=UserRole.CUSTOMER,
)
test_db.add(customer)
test_db.commit()
data = {"username": "customer", "role": "Employee"}
response = customer_client.put("/users/update_role", headers=headers, json=data)
customer = test_db.query(User).filter(User.username == "customer").first()
# ensures that status code is 403 - Forbidden
assert response.status_code == 403
# ensures that the endpoint's logic didn't change the user's role
assert user.role == UserRole.CUSTOMER
Broken Function Level Authorization Automated Detection
In terms of automated detection, a similar approach can be utilised as I presented some time ago in the first article of this series – Web API Security Champion: Broken Object Level Authorization.
Summary
In this series, I’ve already described two authorization-related API security risks, detailed in the following articles:
It’s worth to note that 3 of the top 5 API security risks are related to authorization. Based on the OWASP Top 10 API Security Risks, we learn that authorization checks are often implemented insecurely. They pose a significant risk to the application and, when exploited, may have severe consequences for the application’s users and the company. Based on this knowledge, developers and security engineers should focus especially on authorization mechanisms implemented in their applications to prevent severe security issues.
Considering that the #2 place on the API Security Risks list is reserved for the Broken Authentication vulnerability, we could say that 4 out of the top 5 API security risks are related to authorization and authentication mechanisms.
References
- Damn Vulnerable RESTaurant API Game on GitHub
- OWASP Top 10 API Security Risks
- OWASP Authorization Cheat Sheet