Broken Object Level Authorization
Description
Broken Object Level Authorization is an API vulnerability that allows an attacker to bypass the access control mechanism and perform actions on a chosen object without the required permissions. Objects are usually accessed by unique identifiers, such as integers or UUIDs. An attacker with access to this identifier can read, modify, or delete a given object. This vulnerability often occurs when the authorization mechanisms, which check if a user has permissions to access the object are meant to be implemented within the API endpoint. The vulnerability is commonly refered as IDOR (Insecure Direct Object Reference).
In my career, I observed a number of Broken Object Level Authorization security vulnerabilities. In most cases, this vulnerability did not affect all of the endpoints but only a small subset of API endpoints where a developer could forget about implementing checks within the endpoint’s logic. This is one of the most common vulnerabilities and is listed in the OWASP TOP 10 API Security Risks in the 1st position.
Impact
An attacker with knowledge of an object’s identifier could potentially read, update, modify, or delete the object, depending on the issue. Furthermore, if an integer-based, incremental identifier is used to access the object, an attacker could brute-force identifiers and launch a massive attack against the vulnerable API endpoint. This could have a significant security impact, depending on the business use of the API endpoint.
A few examples of the vulnerability reported via HackerOne can be found below:
- IDOR in PayPal – adding secondary users to manage business accounts
- IDOR in NordVPN – accessing payments data of other users
- IDOR in HackerOne – deleting campaigns
Case Study — Damn Vulnerable RESTaurant API
To demonstrate the vulnerability through a real case example and code, I’ve chosen my open-source project — Damn Vulnerable RESTaurant API. The project is available on my GitHub:
Damn Vulnerable RESTaurant is an intentionally vulnerable web application, with an interactive game focused on developers where they can investigate and fix vulnerabilities directly in the code. I recommend taking a look at this project if you’re a developer, an ethical hacker, or a security engineer.
Vulnerability Description
The Broken Object Level Authorization vulnerability is present in one of the /menu
API endpoints. These endpoints are responsible for reading, modifying, and deleting items available in the restaurant that customers could order. The issue specifically occurs in the DELETE /menu/{ID}
API endpoint, which is intended to be used by the restaurant’s employees to remove items from the menu, for example, to delete a burrito when it’s no longer served. However, in the presented example, any logged-in user is able to delete a menu item!
The vulnerable API endpoint implementation in FastAPI is presented below:
@router.delete("/menu/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_menu_item(
item_id: int,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
utils.delete_menu_item(db, item_id)
return {"ok": True}
As you can see above, the item_id
variable is obtained from the URL and is passed directly to the delete_menu_item
function, which deletes the item. There are no additional authorization checks performed against the current_user
to ensure that only an employee can delete them. It should also be noted that the item_id
is an integer value.
Broken Object Level Authorization Proof of Concept
Attackers may be able to identify such vulnerabilities with relatively low effort, and it’s extremely easy to abuse, which makes this vulnerability highly severe.
For example, the following curl
HTTP request can be sent to delete a menu item with the identifier set to 1
:
curl 'http://localhost:8080/menu/1' \
-X 'DELETE' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0X3VzZXIiLCJleHAiOjE3MTMyOTk1NTN9.bhx6I0XYUjbovaBi1g5xzule9VPKsB429dX1abqOsvI' \
-H 'Origin: http://localhost:8080'
As a result, the menu item will be deleted!
Broken Object Level Authorization Fix
In the presented example, the vulnerability can be easily fixed locally by adding specific authorization checks. The Damn Vulnerable RESTaurant API project follows FastAPI conventions, and authorization logic can be implemented within the API endpoint through dependency injection. The project already provides a RolesBasedAuthChecker
class.
Adding auth=Depends(RolesBasedAuthChecker([UserRole.EMPLOYEE, UserRole.CHEF]))
to the method’s arguments will ensure that user role will be validated before executing implemented logic — deleting a menu item.
The following Python code presents the fixed implementation of the API endpoint:
@router.delete("/menu/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_menu_item(
item_id: int,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
auth=Depends(RolesBasedAuthChecker([UserRole.EMPLOYEE, UserRole.CHEF])),
):
utils.delete_menu_item(db, item_id)
return {"ok": True}
Now, only users with the EMPLOYEE
and CHEF
roles will be able to delete menu items. Fixing the vulnerability locally is relatively easy, right?
Broken Object Level Authorization Recommendations
There are several advisories and from my experience, I recommend the following:
- Deny access by default — follow the least privilege principle and provide access only to users who need it from both a business and a security perspective.
- Implement a proper authorization mechanisms — especially in the context of this vulnerability, I’d recommend to implement ReBAC access model.
- For object identifiers, use unpredictable values as GUIDs (version 4 recommended) — this will prevent from brute-force attempts
- Write unit tests covering unauthorized access — unit tests are extremely useful not only for ensuring quality and preventing regression issues but also for addressing security aspects.
- Perform code reviews within your team — logic bugs and security issues can be identified by your teammates.
Based on the presented case study, the following unit test would be recommended to ensure that a menu item can’t be deleted by an unauthorized user:
def test_delete_menu_item_by_unauthorized_user_returns_401(test_db, anon_client):
menu_item = MenuItem(
name="Item", price=10.99, category="", description="", image_base64=""
)
test_db.add(menu_item)
test_db.commit()
response = anon_client.delete(f"/menu/{menu_item.id}")
# validating response status code
assert response.status_code == 401
# validating if menu item still exists in db
assert test_db.query(MenuItem).count() == 1
Broken Object Level Authorization Automated Detection
In the above example, the vulnerability was identified manually through a code review. It could also be identified dynamically by sending HTTP requests to API endpoints using the browser’s mechanisms, including Swagger, or more advanced tools such as Burp Proxy or ZAP.
However, automated detection of such vulnerability might be a rather challenging approach and may require a customised approach dedicated to the utilised technology or code conventions in the project. From my experience, it’s possible to utilise Dynamic Application Security Testing (DAST), but this requires adjusting the scanner. To make such detection more project-specific, customisable scanners such as Nuclei would be required. Another approach that I would like to present here is utilisig Static Application Security Testing (SAST) — writing a simple Semgrep rule. Semgrep is extremely fast and can be easily placed in CI/CD pipelines to detect vulnerabilities before merging the vulnerable code or to detect vulnerabilities at scale.
Semgrep is a great solution for Static Application Security Testing tool, which I have presented in my previous articles, especially in:
Let’s focus on identifying API endpoints in Damn Vulnerable RESTaurant that use sensitive HTTP methods such as DELETE
, POST
and PUT
, and don’t contain any authorization checks. Also, to limit false positives, let’s focus only on API endpoints that take _id
integer variables as input from the user. This way, it will be possible to detect other cases of Broken Object Level Authorization with minimal engineering effort.
To make this write-up more valuable, I utilized the power of LLMs — ChatGPT in this case, to create a Semgrep rule. The code shown below presents a rule with patterns of potentially vulnerable API endpoints in this specific project (based on the technology and used code conventions):
rules:
- id: missing-auth-in-sensitive-endpoints
patterns:
- pattern-either:
- pattern: |
@router.delete($PATH, ...)
def $FUNC(...):
...
- pattern: |
@router.post($PATH, ...)
def $FUNC(...):
...
- pattern: |
@router.put($PATH, ...)
def $FUNC(...):
...
- pattern-not: |
@router.$METHOD($PATH, ...)
def $FUNC(..., auth=Depends(RolesBasedAuthChecker(...))):
...
- pattern-inside: |
def $FUNC(..., $VAR_ID: int, ...):
...
- metavariable-regex:
metavariable: $VAR_ID
regex: '.*_id$'
message: "Endpoint is missing RolesBasedAuthChecker for sensitive HTTP method."
languages: [python]
severity: ERROR
I had to do minor improvements to the ChatGPT output, but overall, I was amazed by how accurate this rule was! Especially because it took me only 10–15 minutes to create it. This example demonstrates that LLMs are highly valuable for creating such vulnerability detection rules.
Now, let’s save the above rule as a file named missing-auth-in-sensitive-endpoints.yaml
and execute Semgrep with the following command in the Damn Vulnerable RESTaurant project’s directory:
# the repository shown above has to be cloned earlier
semgrep -c missing-auth-in-sensitive-endpoints.yaml . --error
The output of this command is presented below:
┌─────────────┐
│ Scan Status │
└─────────────┘
Scanning 68 files (only git-tracked) with 1 Code rule:
CODE RULES
Scanning 28 files.
...
┌────────────────┐
│ 1 Code Finding │
└────────────────┘
app/apis/menu/service.py
missing-auth-in-sensitive-endpoints
Endpoint is missing RolesBasedAuthChecker for sensitive HTTP method.
44┆ @router.delete("/menu/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
45┆ def delete_menu_item(
46┆ item_id: int,
47┆ current_user: Annotated[User, Depends(get_current_user)],
48┆ db: Session = Depends(get_db),
49┆ ):
50┆ utils.delete_menu_item(db, item_id)
51┆ return {"ok": True}
As you can observe the developed rule identified the vulnerable endpoint which I presented! Unfortunately, at the time of writing this article, there was only one vulnerability like this, but implementing such rule in the pipeline, triggered on Pull Requests will make sure that similar vulnerability will be detected in future before materialising on a production environment.
Summary
In this article, I presented Broken Object Level Authorization vulnerability through a practical example, along with a fix and recommendations that should be valuable for developers and security engineers. I also provided ideas for detecting similar vulnerabilities at scale with Semgrep.