A Deep Dive On The Most Critical API Vulnerability — BOLA (Broken Object Level Authorization)
Intro
In this article, I dig into the details about Broken Object Level Authorization (BOLA) — the most common and most severe API vulnerability today according to the OWASP API Security Project.
Insecure Direct Object Reference (IDOR) and BOLA are the same thing. The name was changed from IDOR to BOLA as part of the project.
We hear about large companies that get breached because of BOLA every week. A few well-known recent examples include: Uber, Verizon, Facebook, T-Mobile, and the list goes on.
Almost every company has APIs that are vulnerable to BOLA and there are currently no “off the shelf” solutions to protect you.
During the creation process of the OWASP Top 10 for APIs, project co-leader Erez Yalon and I were gathering information from different sources about API security and we found that there is no one resource that provides comprehensive information about BOLA. This article should fill that gap.
Please keep in mind that you don’t have to read the whole article to understand BOLA.
This article contains different sections that will direct you through the reading:
- For managers — high-level explanation
- For engineers — technical explanation
- For builders — how software developers can fix BOLA
- For breakers — how pen testers can exploit BOLA
- Q&A for curious engineers — deep dive
For Managers — How To Understand BOLA
Unlike other vulnerabilities, BOLA is not very intuitive and some people struggle to understand it. I found a good metaphor to explain BOLA to a non-technical audience. Feel free to use this at your next cocktail party.
Imagine that you go to a club on Saturday night in San Francisco. The fog has rolled in and it’s pretty chilly so you bring a warm jacket. Once you’re in the door you want to ditch your jacket so it doesn’t get in the way of mingling and enjoying the night so you head to the coat check.
At the coat check, you meet James who takes your jacket and hands you a ticket with a number on it — #26. James then takes your jacket and hangs it on the rack with jackets from all the other clubgoers. You head off into the club to have a few drinks and dance the night away.
After about an hour and a few drinks, you realize this is not your scene and you get tired of yelling over the loud music to try to start conversations. Boredom and a bit of deviousness kick in and you decide to use a pen to change the number on your coat check ticket from #26 to #28. It looks passable.
Back at the coat check, you hand the altered ticket to James, who doesn’t bat an eye as he reaches to the rack to grab jacket #28. You’re in luck, instead of your cheap old jacket #28 is a fancy Chanel jacket and it’s just your size.
What you just did is basically equivalent to the BOLA exploit:
- The coat check room is the vulnerable API endpoint.
- James is the vulnerable code that doesn’t implement authorization checks.
- The jacket is the exposed object.
Now that you know this trick, you can just print fake tickets with all the numbers from 1–100 and steal everyone’s jacket. Evil, right?
In the recent Uber breach, Anand Prakash found a “James” in Uber’s APIs — a vulnerable endpoint that doesn’t perform authorization checks. Instead of handling coat-check tickets, the endpoint receives user IDs. Anand asked this endpoint for information about a user that doesn’t belong to him, and that’s exactly what the API did. Using this exploit, an attacker could potentially write a script to enumerate all the IDs of all the users on Uber and get their data.
How Can We Mitigate BOLA Today?
There are a few ways to protect your APIs and eliminate these types of vulnerabilities:
1. Detection — Vulnerable API endpoints can be detected by code reviews or pentests.
2. Solution Planning — A developer with a deep understanding of the API can define specific authorization policies for the vulnerable endpoint.
3. Fix — The fix is usually just a few lines of code and a developer who’s familiar with the API should be able to apply this fix.
For Engineers — A Deeper Dive On BOLA
Resources In Modern Applications
Modern applications handle many types of resources and each resource might have sub-resources. Let’s take a look at a ride-sharing application as an example:
- User → Rider, Driver
- Trip
- Receipt
- Text conversation between driver & rider → Message
- Vehicle → Car, Scooter, Bike
API Endpoints And Resources
In modern REST APIs, each one of these resources is represented by a JSON object, and each resource usually has an API endpoint that exposes it in different ways. For example:
- Trip JSON representation:
- {“pick_up_location”:”Golden Gate Park”,”destination”:”Dolores Park”}
- API endpoints that are related to “Trip”:
- GET /api/trips/{trip_id} — to fetch a specific trip
- POST /api/trips/{trip_id}/add_tip_to_trip — add tip to an old trip
- POST /api/trips — create a new trip
- DELETE /admin/api/trips/{trip_id} — admin function to delete a trip
API Endpoints And Objects ID
Because one user can have multiple objects from the same resource (e.g. trip), some API endpoints need to know which object (e.g. a specific trip) the user is willing to access. This is where the object ID comes into the picture. The client basically sends the object ID that the user needs to access.
For example, if a user had a great trip with a driver, he can choose the specific trip on the “trip history” view on the application, and add a tip to the trip. Then the mobile client will trigger an API call to “/api/trips/<trip_id>/add_tip” with the ID of the chosen trip.
Modern applications handle many resources and expose many endpoints to access them. Hence, modern APIs expose many object IDs. Those IDs are an integral part of the REST standard and modern applications.
The Challenge
Exposing an object via its ID might be a good software design practice, but once the client can specify the ID, it opens a door to a very dangerous vulnerability — BOLA.
The Exploit
In BOLA, a user accesses objects that he should not have access to by manipulating the IDs.
For example:
An attacker sniffs the traffic between the mobile application and the API, and finds the following API call:
“/api/trips/7891” that returns all the details about the trip — including PII like the rider name and address.
The attacker writes a script to enumerate all the IDs. The script will send many API calls from:
/api/trips/0001
→
/api/trips/9999
And, what a surprise, the attacker leaked all the trips of all of the users in the app.
For Builders — How To Build A Good Authorization Mechanism
A good authorization mechanism that addresses BOLA should contain the following components:
1: Authorization Decision Engine:
The core of the mechanism is an authorization decision engine. This engine should have multiple functions which answer questions such as:
- Does user #717 have access to view receipt #11111
- Does user #666 have access to edit trip #929
- Does admin #222 have access to delete user #555
I don’t want to get into underlying implementation details because:
- I’m not a great developer
- There is no one generic way to do it since it totally depends on the logic of the application.
Let’s take the following examples:
- Receipt #11111 belongs to trip #313. The trip was a “carpool” ride and shared between 3 different users, so the receipt is also shared. All the co-riders of trip #313 should have access to the receipt.
- Admin #222 is a regional admin of Northern California, and she should have access to delete all the users in this area. She wants to delete user #555, but she shouldn’t be able to do it, since user #555 is from NYC.
The decision engine should be planned and built very carefully to answer these questions
2: Use The Decision Engine
Even if you build the best authorization decision engine, it’s worth nothing if nobody uses it.
Let’s imagine you’re the mayor of San Francisco in 2050. You could build the smartest and safest traffic light system, but it’s worth noting if the autonomous cars don’t check — “is the light green?” before they proceed.
Building a good decision engine, without using it, is the exact same thing. Every API endpoint that handles object IDs from clients should use the decision engine.
Let’s assume that a company has implemented a great decision engine that can answer all the authorization questions in the world.
A new engineer joins the company and creates a new controller:
— Vulnerable Code —
It doesn’t matter how great the decision engine is if developers don’t use it. Nobody can prevent you, as a developer, from performing a direct access request to the DB. It’s your responsibility to use the decision engine before accessing the DB based on input from the client.
One simple, naive way to call the decision engine would be:
To summarize:
Part #1 — The decision engine is:
- Smart and complex
- Centralized
Part #2 — Using the decision engine is:
- Simple
- Spread across many different places in the code
For Breakers — How To Exploit BOLA
Disclaimer: This is my comfort zone and I’m really excited to start writing about the fun part of BOLA — exploitation. As a pentester, if you deeply understand how to find and exploit BOLA, your glory is guaranteed :)
APIs are vulnerable to BOLA the same way traditional applications used to be vulnerable to SQL injections 10 years ago.
This is not a guide for beginners, but for people who have some basic experience in pen-testing. If you want to learn from scratch about BOLA, this guide by Sam Huston is a great resource.
There is no one proven way to approach BOLA, but I’m going to share the required mindset and some tips.
How To Think
- Understand the business logic of the application
Don’t be on autopilot and don’t change random IDs in API calls until you see PII in the response. There are automatic tools that can do this for you. Think. - Every time you see a new API endpoint that receives an object ID from the client, ask yourself the following questions:
* Does the ID belong to a private resource? For example, If we talk about some “news” feature, and the API endpoint is “/api/articles/555”, there’s a good chance that all the objects should be public by design since articles are public.
* What are the IDs that belong to me? - Understand relationships between resources
Understand that there’s a relationship between “trips” and “receipts” and that each trip belongs to a specific user. - Understand the roles and groups in the API
Try to understand what the different possible roles in the API are. For example — user, driver, supervisor, manager. - Leverage the predictable nature of REST APIs to find more endpoints
You saw some endpoint that exposes a resource in a RESTy way?
For example: GET /api/chats/<chat_id>/message/<message_id> Don’t be shy, try to replace GET with another HTTP method.
If you receive an error, try to:
* Add a “Content-length” HTTP header
* Change the “Content-type”
How To Test:
The basic way to test for BOLA is to guess the random ID of the object, but this doesn’t always work. The more efficient way would be to perform “Session Label Swapping”:
1. Identify the session label:
A session label is a generic term to describe every string that is used by the API to identify the logged-in user. The reason I call it a session label and not a session ID or authentication token is because, for BOLA exploitation, it doesn’t matter!
2. Log two-session labels from different users:
Create two different users and document their session label.
For example:
User 1, Hugo =
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMTEiLCJuYW1lIjoiSHVnbyIsImlhdCI6MTUxNjIzOTAyMn0.JaTvHH5TbKzgRMa5reRDMiPjHEmPKY8axsu5dFMu5Ao
User 2, Bugo =
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMjIiLCJuYW1lIjoiQnVnbyIsImlhdCI6MTUxNjIzOTAyMn0.uXTOzhX1mnYI3TZUSyjWS7qQXfYNn6qw-MehVGAgvpc
Keep them in a .txt file.
3. Login as user #1, sniff the traffic and find an API call to an endpoint that receives an object ID from the client. For example: /api/trips/e6d84adc-b1c7–4adf-a392–35e5b71f068a/details
4. Repeat and intercept the suspicious API call.
5. Change the session label of user #1 to the session label of user #2 — in order to make a call on behalf of user #2x.
6. Absorb the result. If you received an authorization error, the endpoint is probably not vulnerable.
7. If the endpoint returns details of the object, compare the two results and check if they are the same. If they are the same — the endpoint is vulnerable.
8. Some endpoints don’t return data. For example, the endpoint “DELETE /api/users/<user_id>” would only return a status code without data.
It might be a bit more complicated to understand if they are vulnerable or not, but don’t ignore them.
Tips & Tricks:
1. Object IDs in URLs tend to be less vulnerable. Try to put more effort on IDs in HTTP headers/bodies.
2. GUID instead of numeric values? Don’t give up!
Use the “session label swapping” technique or find endpoints that return IDs of objects that belong to other users.
3. Always try numeric IDs. If you found that an endpoint receives a non-numeric object ID, like a GUID or an email address, give it a shot and try to replace it with a numeric value (e.g.: replace “inon@traceable.ai“ with the number “100”)
4. Received 403/401 once? Don’t give up!
It’s not extremely common, but some weird authorization mechanisms only work partially. Try many different IDs. For example: if the endpoint “api/v1/trips/666” returned 403, run a script to enumerate 50 random IDs from 0001 to 9999.
5. Find the most niche features in the application
Let’s say you found a weird feature to create a customized picture to your profile only during avocado awareness month (it’s June btw), and it performs an API call to /api/avocado_awarness_month/profile_pics/<avocado_emoji_id>.
There’s a very good chance that the developers haven’t thought about authorization there.
How to bypass object level authorization:
1. Wrap the ID with an array.
Instead of {“id”:111} send {“id”:[111]}
2. Wrap the ID with a JSON object
Instead of {“id”:111} send {“id”:{“id”:111}}
3. Try to perform HTTP parameter pollution:
/api/get_profile?user_id=<legit_id>&user_id=<victim’s_id>
OR
/api/get_profile?user_id=<victim’s_id>&user_id=<user_id>
This can work in situations where the component that performs the authorization check and the endpoint itself use different libraries to parse query parameters. In some cases, library #1 would take the first occurence of “user_id” while library #2 would take the second.
4. Try to perform JSON parameter pollution
POST api/get_profile
{“user_id”:<legit_id>,”user_id”:<victim’s_id>}
OR
POST api/get_profile
{“user_id”:<victim’s_id>,”user_id”:<legit_id>}
It’s pretty similar to HTTP parameter pollution.
5. Try to send a wildcard instead of an ID. It’s rare, but sometimes it works.
6. Find API hosts where the authorization mechanism isn’t enabled. Sometimes, in different environments, the authorization mechanism might be disabled, for various reasons. For example: QA would be vulnerable but production would not.
7. Try to perform type-juggling
8. Not common, but a great example of how you can bypass BOLA protection
Q&A For Curious Engineers
Q: What is the object ID?
Usually, the object ID is the actual primary key of the object in the database.
For example, this is the user table:
The primary key would be used in API endpoints that access the user resource as an object ID.
This primary key is usually a sequence number or a GUID.
Other options
Sometimes we see another unique value that is assigned to each record on top of the regular primary key. It could be implemented in different methods, for example, a piece of code that does the conversion or as a separate column in the DB table.
In those cases, the object ID that is exposed to the client through the REST API may be:
- Encrypted value (e.g. encrypted object ID)
- Email address/phone number (instead of user ID)
- Alphanumeric name
Q: Are there different types of BOLA?
Yes. There are two main types of BOLA:
1. Based on user ID.
The API endpoints receive a user ID and access the user object based on this ID. For example:
/api/trips/get_all_trips_for_user?user_id=777
It’s usually easier to solve this type of BOLA, because the authorization mechanism is straight forward — the developers simply fetch the ID of the logged in user from the session (e.g.: `current_user.id`), and compare it with user_id from the GET parameter.
Things get more complicated when one user is supposed to manage other users by design (for example: sub users, regional manager, etc)
2. Based on object ID.
The API endpoint receives an ID of an object which is not a user object. For example:
/api/trips/receipts/download_as_pdf?receipt_id=1111
In many cases, it’s not a simple question — who should have access to this object?
Q: Why is it so common in modern applications?
I asked myself many times “why is BOLA so common in APIs?” After extensive research, I believe that the main reasons are:
Reason 1 — More IDs are sent from the clients
Modern clients send more IDs than before. Why?
A. Servers are less aware of the user’s state in modern applications.
Back in the day, the server might have known the user’s state: what buttons he has clicked, which object he’s watching, etc…
Today it’s a job mostly done by the client-side. In modern applications, the servers are much less aware of the client state.
Instead, the clients send more parameters to the API to reflect the user state by demand.
Let’s say you are using the Uber application, and perform the following steps:
Since you have more than one “trip” object, and the server doesn’t know which trip you clicked (this state is usually maintained by the client), the client has to send the ID of the chosen trip in further API calls.
B. There are more IDs in The REST standard:
The REST standard encourages developers to send IDs in the URL.
Reason 2 — Old tricks to solve BOLA don’t work anymore:
Some old techniques for developers to prevent BOLA don’t really work anymore because of new concepts in modern APIs:
A: Temp tables are not a thing anymore
Back in the day, it was common to see situations where the backend would render a visual table for a user that contains only elements that belong to him. The IDs were temporary for the table and the server would maintain a correlation between temporary IDs and internal IDs in various ways.
Let’s assume we’re talking about a traditional application where you can create sub-users and manage them. The server would render a visual HTML page with a table of all the users you have access to:
This table would also contain buttons, for example — “delete user”.
If the user clicked this button, the client would trigger an HTTP call to a controller that deletes a specific user.