Web API Security Champion: Broken Object Level Authorization (OWASP TOP 10)

April 17, 2024


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:


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 /orders API endpoints. These endpoints are responsible for creating and managing orders made by customers. They are available only for users with Customer role. The vulnerable endpoint allows for obtaining details of any order by any customer which means that one user could access orders made by other users by providing the order_id identifier in the request.

The vulnerable API endpoint implementation in FastAPI is presented below:

@router.get("/orders/{order_id}", response_model=schemas.Order)
def get_order(
    order_id: int,
    db: Session = Depends(get_db),
    auth=Depends(RolesBasedAuthChecker([UserRole.CUSTOMER])),
):
    db_order = db.query(Order).filter(Order.id == order_id).first()
    if db_order is None:
        raise HTTPException(status_code=404, detail="Order not found")
    return db_order

As you can see above, the order_id variable is obtained from the URL and is passed directly to the query that searches for order with given identifier. There are no additional authorization checks performed against the current_user to ensure that only an authorised user can access details of the order. It should also be noted that the order_id is an integer value which makes it easy to iterate for potential attackers.

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 access order details with the identifier set to 1:

curl 'http://localhost:8080/orders/1' \
  -X 'GET' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0X3VzZXIiLCJleHAiOjE3MTMyOTk1NTN9.bhx6I0XYUjbovaBi1g5xzule9VPKsB429dX1abqOsvI' \
  -H 'Origin: http://localhost:8080'

As a result, the order details with delivery address and phone number of a customer will be returned as presented below!

{
  "id": 1,
  "delivery_address": "123 Main St",
  "phone_number": "555-1234",
  "user_id": 1,
  "status": "Pending",
  "items": [
    {
      "menu_item_id": 1,
      "quantity": 1
    }
  ]
}

Broken Object Level Authorization Fix

In the presented example, the vulnerability can be fixed locally by adding an explicit ownership check. 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, but in this case we also need to verify that the authenticated user actually owns the requested order.

Adding a current_user: Annotated[User, Depends(get_current_user)] parameter and then comparing db_order.user_id to current_user.id ensures that only the owner of the order can retrieve it. The following Python code presents the fixed implementation of the GET /orders/{order_id} endpoint:

The following Python code presents the fixed implementation of the API endpoint:

@router.get("/orders/{order_id}", response_model=schemas.Order)
def get_order(
    order_id: int,
    current_user: Annotated[User, Depends(get_current_user)],
    db: Session = Depends(get_db),
    auth=Depends(RolesBasedAuthChecker([UserRole.CUSTOMER])),
):
    db_order = db.query(Order).filter(Order.id == order_id).first()
    if db_order is None:
        raise HTTPException(status_code=404, detail="Order not found")

    if db_order.user_id != current_user.id:
        raise HTTPException(status_code=403, detail="Not authorized to access this order")
    # alternatively, we could return 404 with the same detail message to 
    # prevent from identifying valid order identifiers

    return db_order

Now, only the customer who originally placed the order (i.e., whose user_id matches) will be able to fetch it. Attempting to retrieve another user’s order will return a 403 Forbidden.

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 mechanism—in the context of this vulnerability, consider adopting a resource-based access control (ReBAC) model, where each request enforces both role checks and ownership checks. This makes it easier to centralize and audit who can see or modify which objects.
  • Use unpredictable object identifiers — for order IDs, consider using UUIDv4 instead of sequential integers. This approach reduces the chance of an attacker guessing valid IDs; even if an attacker obtains one valid UUID, they cannot brute-force nearby values.
  • 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 an order can’t be accessed by an unauthorized user:

def test_get_order_by_non_owner_returns_403(test_db, customer_client):
    # Create a menu item to associate with the order
    menu_item = MenuItem(
        name="Special Pizza",
        price=15.99,
        category="Pizza",
        description="A delicious pizza with a special blend of toppings",
        image_base64="",
    )
    test_db.add(menu_item)
    test_db.commit()

    # Create an order and add to the database, uses different id than customer_client
    order = Order(delivery_address="123 Main St", phone_number="555-1234", user_id=999)
    test_db.add(order)
    test_db.commit()

    # Simulate customer B (with different user_id) trying to fetch it
    response = customer_client.get(f"/orders/{order.id}")
    assert response.status_code == 403
    assert response.json().get("detail") == "Not authorized to access this order"


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-ownercheck-in-get-endpoints
    patterns:
      - pattern: |
          @router.get($PATH, ...)
          def $FUNC(..., $CURRENT_USER, ...):
              ...

      - pattern-not-inside: |
          def $FUNC(...):
              ...
              if $OBJ.user_id != $USER.id:
                  ...

      - pattern-inside: |
          def $FUNC(..., $VAR_ID: int, ...):
              ...

      - metavariable-regex:
          metavariable: $VAR_ID
          regex: '.*_id$'

    message: "Endpoint is missing an ownership check (e.g., `db_order.user_id != current_user.id`)."
    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-ownercheck-in-get-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-ownercheck-in-get-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/orders/services/get_order_service.py 
       missing-ownercheck-in-get-endpoints                                           
          Endpoint is missing an ownership check (e.g., `db_order.user_id != current_user.id`).
                                                                                               
           11┆ @router.get("/orders/{order_id}", response_model=schemas.Order)
           12┆ def get_order(
           13┆     order_id: int,
           14┆     db: Session = Depends(get_db),
           15┆     current_user: Annotated[User, Depends(get_current_user)],
           16┆     auth=Depends(RolesBasedAuthChecker([UserRole.CUSTOMER])),
           17┆ ):
           18┆     db_order = db.query(Order).filter(Order.id == order_id).first()
           19┆     if db_order is None:
           20┆         raise HTTPException(status_code=404, detail="Order not found")
           21┆ 
           22┆     return db_order

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.

It should be noted that the above Semgrep rule has mainly educational value and may not be highly effective for all codebases as may produce some false positives. However, still it works well for our case study.


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.

References

Interesting Article?

Subscribe to Receive Recent Articles!

You will receive articles related to DevSecOps and application security published at DevSec Blog directly to your email address.



Subscribe
Notify of
guest
2 Comments
Inline Feedbacks
View all comments
NaSangWon
NaSangWon
27 days ago

Hi. Thank you for providing this great app and articles.

But I have a question. I think this vulnerability is BFLA and not BOLA because, according to the OWASP definition…

https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/

In the case of BOLA, it’s by design that the user will have access to the vulnerable API endpoint/function. The violation happens at the object level, by manipulating the ID. If an attacker manages to access an API endpoint/function they should not have access to – this is a case of Broken Function Level Authorization (BFLA) rather than BOLA.

So this is BFLA because customer can delete menu item that they don’t have the privileges to do so. What do you think?