StackHawk
Hamburger Icon

Java Broken
Object Level
Authorization Guide

stackhawk

StackHawk|May 12, 2022

Java broken object level authorization exposes sensitive application data to attackers because of poor access control. Here's how to fix it.

Java Broken Object Level Authorization Guide: Examples and Prevention
 image

Broken object-level authorization (BOLA) happens when a user has the ability to gain access to information that only a system administrator should see. This means that attackers have access to sensitive resources, like confidential user data or business secrets. It results in catastrophic security breaches. So, web application developers need to understand this security problem and how to avoid it. 

This article will discuss broken object-level authorization (BOLA) problems in Java applications. Then, we'll look at a few examples and how you can fix and prevent the problem. 

Broken Object Level Authorization

Broken object level authorization happens when an application fails to check a user's entitlements before granting them access to information. As a result of application developers failing to control access to individual resources, authenticated users are able to retrieve, modify, create and delete information that they shouldn't. 

There are plenty of third-party services and libraries for authentication, but the responsibility for verifying access control at the object level falls to the application. This check is often based on user information provided by the authenticator, but using that information for performing a final check is the responsibility of the app code. 

These bugs are easy to exploit since all the hackers need are a set of credentials and a script to retrieve the data. So once an attacker discovers a BOLA problem, they exploit the application very quickly. 

A Broken Object Level Authorization Example

A Simple API

Consider a website that stores user information using a simple RESTful API. Here's a simple JSON definition of a web site user: 

{
    "id": 1,
    "fullName": "One More Person",
    "jobTitle": "Yet Another Title",
    "email": "foo@example.com"
}

The application offers typical CRUD endpoints: 

  • GET /people/ with no argument retrieves a list of all users.

  • PUT /people/{ID} with a JSON record updates an existing user with new information.

  • POST /people/ with a JSON record to add a new user.

  • GET /people/{ID} to retrieve a user by Id.

  • DELETE /people/{ID} deletes a user.

A BOLA API Issue

Leaking personal information about users can result in serious civil and legal consequences. So, managing user information requires extra care. Depending on the nature of the application, there may be reasons for users to see information about each other. But, even that requires extra care. 

So at a minimum, the website should provide object-level authorization like this: 

  1. Site administrators can add new users, update their information, and delete them.

  2. Users can update their own information and remove themselves.

  3. Users can retrieve each other's information.

Item #3 may or may not be appropriate based on the site. The rules governing access to user access are beyond the scope of this article. Depending on the type of information, providing users access to each other's data may be a case of Excessive Data Exposure. 

Regardless of the details, checking to see if a user has a valid session isn't enough. The application needs to verify that a user is valid and then ensure that they are allowed to complete their request. 

Java Broken Object Level Authorization

Sample Java RESTful API

Let's look at an application based on the sample code included in Dropwizard's GitHub repo. In order to support this tutorial, I've made a few small modifications. So if you pull the code from Github you'll see some differences. 

Here's a modified version of the example PeopleResource class. All the endpoints defined above are implemented in this class, and the users must authenticate with Basic Authentication to access the endpoint. 

@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("BASIC_GUY")
public class PeopleResource {

    private final PersonDAO peopleDAO;

    public PeopleResource(PersonDAO peopleDAO) {
        this.peopleDAO = peopleDAO;
    }

    @POST
    @UnitOfWork
    public Person createPerson(@Valid Person person) {
        return peopleDAO.create(person);
    }

    @PUT
    @Path("/{personId}")
    @UnitOfWork
    public Person updatePerson(@Valid Person person) {
        return peopleDAO.update(person);
    }

    @GET
    @Path("/{personId}")
    @UnitOfWork
    public Person getPerson(@PathParam("personId") OptionalLong personId) {
        return findSafely(personId.orElseThrow(() -> new BadRequestException("person ID is required")));
    }

    @DELETE
    @Path("/{personId}")
    @UnitOfWork
    public Person deletePerson(@PathParam("personId") OptionalLong personId) {
        return peopleDAO.deleteById(personId.orElseThrow(() -> new BadRequestException("person ID is required")));
    }

    private Person findSafely(long personId) {
        return peopleDAO.findById(personId).orElseThrow(() -> new NotFoundException("No such user."));
    }
}

On line #3, the @RolesAllowed() annotation limits access to these endpoints to authenticated users with the BASIC_GUY role. As a result, the user must be authenticated before they can access any methods in this class. 

This role is defined here: 

public class ExampleAuthenticator implements Authenticator<BasicCredentials, User> {
    /**
     * Valid users with mapping user -> roles
     */
    private static final Map<String, Set<String>> VALID_USERS = Collections.unmodifiableMap(Maps.of(
        "guest", Collections.emptySet(),
        "good-guy", Collections.singleton("BASIC_GUY"),
        "chief-wizard", Sets.of("ADMIN", "BASIC_GUY")
    ));

    @Override
    public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
        if (VALID_USERS.containsKey(credentials.getUsername()) && "secret".equals(credentials.getPassword())) {
            return Optional.of(new User(credentials.getUsername(), VALID_USERS.get(credentials.getUsername())));
        }
        return Optional.empty();
    }
}

Unauthenticated users have no role. The user named "good-guy" has the BASIC_GUY role. The "chief-wizard" user has both the ADMIN and BASIC_GUY roles. 

Java Broken Object Level Authorization Guide: Examples and Prevention
 image

Java Broken Object-Level Authorization in Action

Let's take this example for a test drive. 

Here's the log of an unauthenticated request with Postman

GET http://localhost:8080/people/1
Content-Type: application/json
 
HTTP/1.1 401 Unauthorized
Date: Wed, 04 May 2022 18:52:49 GMT
WWW-Authenticate: Basic realm="SUPER SECRET STUFF"
Content-Type: text/plain
Content-Length: 49
 
Credentials are required to access this resource.

Unauthenticated users can't view users. That's what we'd expect. 

Let's try with the "good-guy" login. 

GET http://localhost:8080/people/1
Content-Type: application/json
Authorization: Basic Z29vZC1ndXk6c2VjcmV0

HTTP/1.1 200 OK
Date: Wed, 04 May 2022 19:12:26 GMT
Content-Type: application/json
Vary: Accept-Encoding
Content-Length: 94
 
{"id":1,"fullName":"One More Person","jobTitle":"Yet Another Title","email":"foo@example.com"}

That user is able to view users. 

Let's update that user's email. 

PUT http://localhost:8080/people/1
Content-Type: application/json
Authorization: Basic Z29vZC1ndXk6c2VjcmV0
 
{
"id": 1,
"fullName": "One More Person",
"jobTitle": "Yet Another Title",
"email": "differentemail@example.com"
}
 
HTTP/1.1 200 OK
Date: Wed, 04 May 2022 19:19:06 GMT
Content-Type: application/json
Content-Length: 105
 
{"id":1,"fullName":"One More Person","jobTitle":"Yet Another Title","email":"differentemail@example.com"}

That worked, too. "Good-guy" can modify users. 

The same account can add them, too. 

POST http://localhost:8080/people/
Content-Type: application/json
Authorization: Basic Z29vZC1ndXk6c2VjcmV0
 
{
"fullName": "Jane Doe",
"jobTitle": "Branch Manager",
"email": "jane.doe@example.com"
}
 
HTTP/1.1 200 OK
Date: Wed, 04 May 2022 20:03:25 GMT
Content-Type: application/json
Content-Length: 89
 
{"id":9,"fullName":"Jane Doe","jobTitle":"Branch Manager","email":"jane.doe@example.com"}

The code limits access to the PeopleResource class to members of BASIC_GUY. As a result, members of that class can call all of its endpoints. 

But, this class has BOLA. Any user with access to the application can not only view user information but can modify, add, and delete them, too. 

So, how do we fix this? 

Fixing Java Broken Object-Level Authorization

You fix Broken Object-Level Authorization by adding explicit authorization for privileged operations. 

The first step is to identify the operations to which we don't want BASIC_GUYs to have access. For our sample application, that's easy. Only members of ADMIN should be able to add, modify, and delete users. 

Dropwizard makes this easy. We can add the @RolesAllowed() annotation to methods, too. 

First, let's limit the ability to add new users to ADMIN first: 

 @POST
    @UnitOfWork
    @RolesAllowed("ADMIN")
    public Person createPerson(@Valid Person person) {
        return peopleDAO.create(person);
    }

Now, we'll try to add a new user as "good-guy": 

POST http://localhost:8080/people/
Content-Type: application/json
Authorization: Basic Z29vZC1ndXk6c2VjcmV0
 
{
"fullName": "Alfred E. Neumann",
"jobTitle": "Janitor",
"email": "whatmeworry@example.com"
}
 
HTTP/1.1 403 Forbidden
Date: Wed, 04 May 2022 20:16:45 GMT
Content-Type: application/json
Content-Length: 45
 
{"code":403,"message":"User not authorized."}

Perfect. 

Next, let's try as "chief-wizard." You can see in the log that the encrypted Authorization token holds a different value: 

POST http://localhost:8080/people/
Content-Type: application/json
Authorization: Basic Y2hpZWYtd2l6YXJkOnNlY3JldA==
 
{
"fullName": "Alfred E. Neumann",
"jobTitle": "Janitor",
"email": "whatmeworry@example.com"
}
 
HTTP/1.1 200 OK
Date: Wed, 04 May 2022 20:20:33 GMT
Content-Type: application/json
Content-Length: 95
 
{"id":10,"fullName":"Alfred E. Neumann","jobTitle":"Janitor","email":"whatmeworry@example.com"}

The API request succeeded. 

But can "good-guy" still see the new user? Here's the request with the original token: 

GET http://localhost:8080/people/10
Content-Type: application/json
Authorization: Basic Z29vZC1ndXk6c2VjcmV0
 
{
"fullName": "Alfred E. Neumann",
"jobTitle": "Janitor",
"email": "whatmeworry@example.com"
}
 
HTTP/1.1 200 OK
Date: Wed, 04 May 2022 20:22:25 GMT
Content-Type: application/json
Vary: Accept-Encoding
Content-Length: 95
 
{"id":10,"fullName":"Alfred E. Neumann","jobTitle":"Janitor","email":"whatmeworry@example.com"}

"Good-guy" can still list users. 

So, we can finish the job by adding the annotation to updatePerson and deletePerson. 

@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("BASIC_GUY")
public class PeopleResource {

    private final PersonDAO peopleDAO;

    public PeopleResource(PersonDAO peopleDAO) {
        this.peopleDAO = peopleDAO;
    }

    @POST
    @UnitOfWork
    @RolesAllowed("ADMIN")
    public Person createPerson(@Valid Person person) {
        return peopleDAO.create(person);
    }

    @PUT
    @Path("/{personId}")
    @UnitOfWork
    @RolesAllowed("ADMIN")
    public Person updatePerson(@Valid Person person) {
        return peopleDAO.update(person);
    }

    @GET
    @Path("/{personId}")
    @UnitOfWork
    public Person getPerson(@PathParam("personId") OptionalLong personId) {
        return findSafely(personId.orElseThrow(() -> new BadRequestException("person ID is required")));
    }

    @DELETE
    @Path("/{personId}")
    @UnitOfWork
    @RolesAllowed("ADMIN")
    public Person deletePerson(@PathParam("personId") OptionalLong personId) {
        return peopleDAO.deleteById(personId.orElseThrow(() -> new BadRequestException("person ID is required")));
    }

    private Person findSafely(long personId) {
        return peopleDAO.findById(personId).orElseThrow(() -> new NotFoundException("No such user."));
    }
}
Find and Fix Application Security Vulnerabilities with Automated Testing

Avoid Java Broken Object-Level Authorization

In this article, we've looked at Broken Object Level Authorization and how it can lead to serious data compromises. We saw how application design that doesn't address access control on the object level allows unprivileged users to add, remove and update the information they shouldn't be allowed to. Then, after an overview of the problem, we looked at sample java code that BOLA. After that, we addressed the issue by adding object level authorization where it was needed. Dropwizard and most Java frameworks have the mechanisms you need to address this serious problem. 

This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!).


StackHawk  |  May 12, 2022

Read More

Add AppSec to Your CircleCI Pipeline With the StackHawk Orb

Add AppSec to Your CircleCI Pipeline With the StackHawk Orb

Application Security is Broken. Here is How We Intend to Fix It.

Application Security is Broken. Here is How We Intend to Fix It.

Using StackHawk in GitLab Know Before You Go (Live)

Using StackHawk in GitLab Know Before You Go (Live)