developers

How Fine Grained Authorization Solves the Most Critical API Security Risk

Broken Object Level Authorization is the most critical API security vulnerability. Learn how Fine Grained Authorization (FGA) can help fix it.

According to OWASP, the worldwide web security community, Broken Object Level Authorization (BOLA) is the most critical vulnerability affecting APIs. What is this vulnerability? How can it be exploited? How can you prevent it in your API? Keep reading to find the answers to these questions.

What Is OWASP API Security Top Ten?

One of the main goals of the OWASP community is to make developers and technologists aware of the risks their applications and systems are exposed to. With this in mind, the community publishes some documents that highlight what are the most common security risks.

The OWASP Top Ten document is the most popular one. It focuses on web development in general. But there is also a more specific document for APIs: the API Security Top Ten.

Both the 2019 and the 2023 versions of the document report that Broken Object Level Authorization is the most critical security risk. It's incredible that after four years this security risk is still affecting APIs around the world! Is there a way to mitigate if not eliminate this vulnerability?

The answer to this question is positive: you can prevent this vulnerability from affecting your API by designing a proper authorization system. But before you get to the solution, let's learn what Broken Object Level Authorization is.

What Is Broken Object Level Authorization?

Authorization is the process of determining what a user has access to. This access control can be done in many different ways based on the business rules of the specific application.

In the API context, each request to a protected endpoint should be analyzed to determine if the client is authorized to access and perform the requested action. Typically, this check is done at the code level and is based on verifying that the client has the right permissions to access the resource (the object) exposed by the API endpoint.

The main problem in this case is to define what is the actual resource to protect.

In some scenarios, it's the API endpoint itself. Consider the

/profile
endpoint: it implicitly returns the profile data of the current user. The API may know who the current user is from the session storage or from the access token sent along with the request.

In other scenarios, the object to protect is the requested resource. Consider the endpoint

profile/42
, which returns the profile data of the user with ID 42. In this case, comparing the current user ID to the requested user ID may not be correct. Think of an administrator who should have access to the profile data of all the users. The authorization check should verify that the current user has permission to access the requested object.

Inaccurate permission checking in endpoints that use object IDs exposes the API to BOLA attacks. In fact, an attacker could manipulate the object ID to gain access to an object for which they have no permissions.

In the past, the Broken Object Level Authorization (BOLA) vulnerability was called Insecure Direct Object Reference (IDOR). The name was changed because it suggested that the problem was with the ID, while in fact it's with the authorization check.

An Example of BOLA Vulnerability

To understand how a Broken Object Level Authorization vulnerability can be exploited by an attacker, let's use a practical example.

Suppose your bank provides you with an app to manage your account. Being a curious developer, you take a look at how your app interacts with the server. You notice that it calls the endpoint

https://thebankwebsite.com/accounts/9876543
to get data about your own account whose number is 9876543.

You remember that one of your friends has the same bank as you. You also remember that you made a money transfer to him in the past, so you look for his account number in the messages he has sent you. You find out that his account number is 1029387. So, you take curl or another HTTP client and replay your HTTP request to the bank's server by changing the endpoint's URL:

https://thebankwebsite.com/accounts/1029387
.

Surprisingly, you get the data about your friend's account from the server! This means that the bank's API has a BOLA vulnerability. Their authorization check fails at the object access level.

You can imagine, for example, that the API implementation for that particular endpoint is as follows:

const express = require('express');
const app = express();
const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

//This checks that the provided access token is valid
const validateAccessToken = auth({
  audience: '...yourApiIdentifier...',
  issuerBaseURL: `https://...yourDomain.../`,
});

//This checks that the provided access token grants reading permissions
const checkReadPermissions = requiredScopes('read:account');

app.get('/accounts/:accountNumber', 
        validateAccessToken, 
        checkReadPermissions,
        function(req, res) {
          res.json(getAccountData(req.params.accountNumber));
        }
);

This Node.js Express code snippet shows how a GET request on the

/accounts/:accountNumber
endpoint is handled. Note that the endpoint is protected by two middleware:

  • validateAccessToken
    , which ensures that the client provides a valid access token.
  • checkReadPermissions
    , which ensures that the access token contains the required
    read:account
    scope in order to allow the client to access the requested account.

Unfortunately, these checks are not sufficient to implement an accurate access to the account object. In fact, they will not prevent you from accessing your friend's account.

Learn web security through a hands-on exploration of some of the most notorious threats.

Download the free ebookSecurity for Web Developers

How to Prevent BOLA Vulnerabilities

What measures can you take to prevent BOLA vulnerabilities?

A common suggestion is to avoid using easily recognizable object identifiers in your endpoint URL, such as the bank account number in the example above. This measure prevents the knowledge of the object ID from allowing an attacker to compose a specific HTTP request. For example, you could use a GUID in the URL to identify the bank account instead of the bank account number itself. GUIDs are randomly generated values and make them very difficult to guess.

This approach can mitigate the risk of BOLA attacks, but it doesn't solve the problem. If the attacker finds a way to determine the GUID associated with a bank account, this protection will be bypassed.

The real problem is not in the object ID but in the authorization mechanism. The access control of your API should not be limited to controlling access to the endpoint. It should verify that only authorized users can access the object exposed by the endpoint.

In the bank account example, this means that additional checks should be performed. For example, you can check that the current user is the owner of the account, as outlined below:

// ...existing code...

app.get('/accounts/:accountNumber', 
        validateAccessToken, 
        checkReadPermissions,
        function(req, res) {
          const userId = req.auth.payload.sub;
          const account = getAccountData(req.params.accountNumber);
  
          if (account.userId == userId) {
            res.json(account);  
          } else {
            res.status(403).json("Permission denied."); 
          }
        }
);

This would solve the unauthorized access to the bank account described earlier. However, it might not cover other scenarios. For example, consider a bank account with multiple owners. Each owner could have different permissions on the account. Also consider a bank employee: they should be able to access the accounts of their branch customers. And perhaps a regional manager should be able to access the accounts of multiple branches.

As you can see, the authorization policy that your API should implement strictly depends on the specific business logic. This logic is mainly determined by the relationship between users and resources. When this relationship becomes complex, your API code run the risk of getting messed up.

What Is Fine-Grained Authorization?

To handle complex authorization policies, you should use Fine Grained Authorization (FGA). FGA allows you to control access to resources by analyzing a context described by various attributes and relationships. By having access to a detailed context, you can more effectively control who can access what in your system.

You can design and implement FGA in a variety of ways, but the most common models are as follows:

  • Attribute-Based Access Control (ABAC). This authorization model allows you to make authorization decisions by evaluating attributes such as the user's role or age, the resource type or size or status, the action requested, the current date and time, and so on.
  • Policy-Based Access Control (PBAC). This authorization model allows you to make authorization decisions based on policies. It is very similar to ABAC. The main difference is that PBAC focuses more on logic while ABAC relies more on data.
  • Relationship-Based Access Control (ReBAC). This authorization model focuses on the relationship between users and resources. This also includes relationships between resources. ReBAC provides a more expressive and powerful authorization model that can describe very complex contexts.

Okta offers Fine Grained Authorization through two solutions based on the ReBAC model:

  • Auth0 FGA. This is an authorization service that centralizes your authorization logic and data. It allows you to define your authorization model, store your authorization data, and make your authorization decisions across all your applications. Read the Auth0 FGA documentation to learn more.
  • OpenFGA. This is the Open Source project powering the Auth0 FGA service. You can install the OpenFGA Server and use its SDKs to integrate authorization into your applications.

How FGA Solves BOLA Vulnerabilities

Auth0 FGA and OpenFGA allow you to fix Broken Object Level Authorization vulnerabilities in your APIs without messing up your code. Whether you use the hosted product or install the Open Source engine on your servers, FGA allows you to centralize the access control to the resources protected by your API.

You can do this in three steps:

  1. Define your authorization model. In this step, you define the types of objects in your system and their possible relationships with each other. The authorization model is usually fairly static once written. Check out this document to learn more about defining your authorization model.
  2. Store your authorization data. This step allows you to define the actual data and its relationships according to the authorization model you defined in the previous step. This represents the state of the system and will be changing as users interact with your application.
  3. Add authorization check to your API. In the third step, you integrate authorization checks into your API and start leveraging the power of FGA.

Define your authorization model

Going back to the bank account example, you can define your authorization model in Auth0 FGA by using its configuration language. The following code shows a minimal example of how your model could be written:

model
  schema 1.1
type user
type branch
  relations
    define employee: [user]
type account
  relations
    define branch: [branch]
    define owner: [user]
    define can_create: employee from branch
    define can_view: owner or employee from branch

Notice that the last line in the model above defines the account owner and the employees of the branch where the account was opened as the legitimate viewers. This is the basis for avoiding the Broken Object Level Authorization problem.

Using the Auth0 FGA Playground, you can also get a visual representation of this model:

The bank authorization model in the Auth0 FGA Playground

You can play with the model used in this article by making your own copy.

Store your authorization data

As the next step, you fill this model with actual data. You define your users and add a direct relationship to the objects in your model. You also define relationships between objects and other aspects that depend on your specific model. You provide data to the system using tuples. For example, the following tuples define some relationships related to the bank model presented earlier:

[
  // User Mary is the owner of the account 12345
  {
    "user": "user:mary",
    "relation": "owner",
    "object": "account:12345"
  },
  // User John is an employee of the branch b1
  {
    "user": "user:john",
    "relation": "employee",
    "object": "branch:b1"
  },
  // Account 12345 was created in the branch b1
  {
    "user": "branch:b1",
    "relation": "branch",
    "object": "account:12345"
  }
]

To learn how to populate your authorization model with data, read this document.

Add authorization check to your API

Finally, you can add the authorization checks to your API using one of the available SDKs. Referring back to the bank example, the Node.js Express code will perform its authorization checks as follows:

const { OpenFgaApi } = require('@openfga/sdk');
const fgaClient = new OpenFgaApi({
  //...configuration...
});

// ...existing code...

app.get('/accounts/:accountNumber', 
        validateAccessToken, 
        checkReadPermissions,
        function(req, res) {
          const userId = req.auth.payload.sub;
          const account = getAccountData(req.params.accountNumber);
  
          const { allowed } = await fgaClient.check({
            tuple_key: {
              user: `user:${userId}`,
              relation: 'can_view',
              object: `account:${req.params.accountNumber}`,
            },
          });
  
          if (allowed) {
            res.json(account);  
          } else {
            res.status(403).json("Permission denied."); 
          }
        }
);

The authorization check is performed by

fgaClient.check()
, which passes the tuple to be verified to the FGA engine. The FGA engine uses the authorization model and the data you provided to determine if the current user is authorized to view the requested account. Since you have defined there who the legitimate viewers of this account are, your API is not BOLA vulnerable. In addition, your code is kept clean.

Read more about integrating FGA into your API.

Try out Auth0 authentication for free.

Get started →

Summary

By the end of this article, you should have a clearer idea of what Broken Access Level Authorization is and how Fine Grained Authorization can help prevent it.

You have begun to explore what BOLA is and how it can be exploited with a practical example. You have seen that the root cause of this vulnerability is an inadequate authorization system. You learned about FGA and saw what Auth0 offers in this area.

Using the API example analyzed earlier, you learned how to integrate it with Auth0 FGA to overcome the BOLA vulnerability in three steps: define an authorization model, populate the model with data, and invoke the authorization check from the API.

In addition to avoiding BOLA vulnerabilities, Auth0 FGA provides a powerful authorization system to solve very complex scenarios effectively and efficiently. Give it a try.