renan@home:~$

Understanding BOLA (Broken Object Level Authorization)

What is object level authorization?

Object level authorization is a mechanism by which a developer ensures that users can only access the specific objects they are authorized to interact with. But how is this achieved in practice?

Let’s suppose the developer has an endpoint that retrieves a user’s profile data. In this case, the system needs to verify that the user requesting the data is indeed authorized to access that specific profile and not someone else’s.

[ApiController]
[Route("[controller]")]
[Authorize]
public class UserController : ControllerBase {
    private IUserService _userService { get; set; }

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet]
    public async Task<ActionResult> GetProfileInfo([FromQuery] int id)
    {
        var user = await _userService.GetuserById(id);
        if (user is null)
        {
            return NotFound("User not found");
        }

        return Ok(new UserModel
        {
            Id = user.Id,
            Email = user.Email,
            Name = user.Name,
        });
    }
}

Let’s take a closer look at this code. The method retrieves user data based on the id query parameter. But who guarantees that the user will send their own ID?

What if a malicious user changes the id in the request, trying to access someone else’s data? Since there is no verification to ensure the id belongs to the authenticated user, they might succeed in retrieving another user’s profile information. This scenario is a classic example of Broken Object Level Authorization (BOLA), where the lack of proper authorization checks at the object level exposes sensitive data to unauthorized users.

In this example, the system trusts that the user will send the correct id, but if an attacker sends a request with a different id, they could gain access to data that doesn’t belong to them, leading to a significant security issue.

Let’s improve our example. Imagine we have an endpoint responsible for changing a user’s password:

[ApiController]
[Route("[controller]")]
[Authorize]
public class UserController : ControllerBase {
    private IUserService _userService { get; set; }
    private ICryptoService _cryptoService { get; set; }

    public UserController(IUserService userService, ICryptoService cryptoService)
    {
        _userService = userService;
        _cryptoService = cryptoService;
    }

    [HttpPost]
    public async Task<ActionResult> ChangePass([FromBody] PassChangeModel passChangeModel)
    {
        var user = await _userService.GetUserById(passChangeModel.Id);
        if (user is null)
        {
            return NotFound("User not found");
        }

        user.Password = _cryptoService.Hash(passChangeModel.NewPassword);

        var passwordChanged = await _userService.UpdateUser(user);
        if (passwordChanged) 
        {
            return Ok("Password changed successfully!");
        }
        
        return BadRequest("Password change failed");
    }
}

As mentioned earlier, a malicious user could easily manipulate the Id field in the request payload, attempting to change the password of another user. Since this code does not perform any validation to ensure the Id belongs to the authenticated user, anyone with a valid Id and token can change the password of any user in the system, leading to a critical security flaw. This further exemplifies the risk posed by Broken Object Level Authorization (BOLA), where the lack of proper authorization checks allows unauthorized users to perform actions on objects they should not have access to.

How to fix the vulnerability

In this improved version, we extract the user ID directly from the JWT token claims.

[ApiController]
[Route("[controller]")]
[Authorize]
public class UserController : ControllerBase {
    private readonly IUserService _userService;
    private readonly ICryptoService _cryptoService;

    public UserController(IUserService userService, ICryptoService cryptoService)
    {
        _userService = userService;
        _cryptoService = cryptoService;
    }

    [HttpPost]
    public async Task<ActionResult> ChangePass([FromBody] PassChangeModel passChangeModel)
    {
        var loggedInUserId = int.Parse(User.Claims.First(c => c.Type == "id").Value);

        var user = await _userService.GetUserById(loggedInUserId);
        if (user is null)
        {
            return NotFound("User not found.");
        }

        user.Password = _cryptoService.Hash(passChangeModel.NewPassword);

        var passwordChanged = await _userService.UpdateUser(user);
        if (passwordChanged) {
            return Ok("Password changed successfully!");
        }

        return BadRequest("Password change failed.");
    }
}

Here’s a detailed explanation of why this approach is more secure and ideal for preventing Broken Object Level Authorization (BOLA) vulnerabilities.

Code Explanation

  • Authentication and Extracting the User ID via JWT Claims:
    • At the start of the method, the code extracts the loggedInUserId directly from the JWT token claims using the following line:

        var loggedInUserId = int.Parse(User.Claims.First(c => c.Type == "id").Value);
      

    JWT (JSON Web Token) is a secure method of transmitting information between the client and the server. It contains digitally signed claims that verify the user’s identity. In this case, the “id” claim stores the authenticated user’s ID, which was assigned during the authentication process.

    • This ID retrieved from the JWT is trustworthy because:

      • The token is digitally signed, so any tampering or forgery would be detected.
      • It is issued after successful authentication, confirming that the user holds valid credentials.

    By directly using the ID from the JWT, we eliminate the need to trust the Id provided in the request body, which significantly reduces the risk of malicious request manipulation.

  • Fetching the User Using the ID from the JWT Token:
    • After obtaining the loggedInUserId from the JWT, the code uses this ID to fetch the authenticated user’s data directly:

        var user = await _userService.GetUserById(loggedInUserId);
      

      This step is crucial for security because it ensures that the data being processed belongs to the user who is actually logged in. There is no need to verify if the Id in the request body matches the one in the JWT token, as we are not trusting any client-supplied Id. This eliminates any possibility of an attacker manipulating the request to alter another user’s data.

  • Protection Against BOLA (Broken Object Level Authorization):

    • The BOLA (Broken Object Level Authorization) vulnerability occurs when an application allows users to access or modify resources they don’t own by failing to properly validate authorization at the object level. In the original example, an attacker could change the Id in the request body and attempt to access or modify another user’s data.

    • By using the loggedInUserId directly from the JWT and ignoring any Id provided by the user in the request body, this approach:

      • Prevents tampering with sensitive data, such as changing another user’s password.

      • Enforces object-level authorization, as each action is tied directly to the authenticated and validated user ID from the JWT.

    This completely eliminates the possibility of a user altering another user’s data, even if they try to modify the request payload.

  • Why is This Solution More Secure?

    • Reduced attack surface: There is no dependency on untrusted data, such as the Id provided by the client. Everything is based on secure information from the JWT token.

    • Strong authorization: The action is strictly limited to the authenticated user. Even if an attacker tries to modify the request body, the password change will only apply to the user who holds the valid JWT token.

    • Better access control: Since the authenticated user’s ID is directly extracted from the JWT token, the permissions are automatically tied to this ID, ensuring that the user can only modify their own data.

References

Owasp