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:
Site administrators can add new users, update their information, and delete them.
Users can update their own information and remove themselves.
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 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."));
}
}
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!).