Keeping our systems secured and robust can be challenging when facing so many threats on a daily basis. One such threat, called broken object-level authorization, was referred to as the number one threat APIs worldwide face today by the OWASP security organization.
This article aims to give you the knowledge and expertise necessary to mitigate this threat. To achieve that, we will be examining what broken object-level authorization is, how attackers abuse these vulnerabilities, and what we can do about it.
We'll start by defining broken object-level authorization. Then, we will provide examples of the two types of broken object-level authorization attacks. And lastly, we will provide you with mitigation strategies against this threat so you can successfully protect your platforms.
By the end of this article, you'll be able to spot broken object-level authorization vulnerabilities and have the tools and knowledge to handle them.
NOTE: This article is aimed at NodeJS and Javascript developers. If you have no experience working with NodeJS or Javascript, you might struggle to get value from this article.
NodeJS Broken Object-Level Authorization
As described by the OWASP in their 2023 report on API security:
"Attackers can exploit API endpoints that are vulnerable to Broken Object Level Authorization by manipulating the ID of an object that is sent within the request. This may lead to unauthorized access to sensitive data. This issue is extremely common in API-based applications because the server component usually does not fully track the client's state, and instead, relies more on parameters like object IDs, that are sent from the client to decide which objects to access."
Now, that is a handful. Don't worry if you can't fully grasp it yet. We will go through the basics to expand on their information.
However, we must understand what authorization is and its nuance before addressing the question.
As we have previously explained in our Rails article:
"In the context of software security, authorization is the mechanism that provides or denies access to resources in a system. Its main job is to ensure that only the people and services authorized to access these resources (data mainly) can do so. This means that without a proper and robust authorization mechanism, all data in a system is at high risk of falling into the wrong hands."
Briefly, authorization systems are the policy enforcement mechanism for your application. They provide the infrastructure to maintain trust in the security of your system and its data.
What Is Broken Object-Level Authorization?
Translating what the OWASP report stated, now we can summarize that broken object-level authorization, or BOLA, is a specific attack that targets weak or poorly implemented authorization mechanisms.
This attack exploits endpoints that allow user input to retrieve objects (data) without proper user authorization validation.
Basically, a bad actor can target your application whenever it doesn't correctly validate that the user requesting a resource has access to it.
Examples of NodeJS Broken Object-Level Authorization
Since we now understand the theory behind BOLA attacks, let's see some examples. There are two types of BOLA attacks that comprise the vast majority of threats:
Attacks That Target Endpoints That Use User ID
In this attack, the attacker focuses on endpoints that receive a form of user ID to retrieve resources. The vulnerable API endpoint will receive the provided user ID and try to access the user object.
One example of an attack would be something as simple as the following:
/* securefirm.com/secureapi/user/get_records_for_user/111 */
app.get("/secureapi/user/get_records_for_user/:user_id", authenticateToken, (req, res) => {
const { id } = req.params;
const record = db.records.filter((a) => {
return a.user_id === id;
})[0];
res.json(record);
});
Of course, this kind of vulnerability is a massive oversight in terms of security. That's why it is imperative to keep an eye on these gaps in security at the development stage.
Attacks That Target Endpoints That Use Object ID
This attack also targets endpoints that receive user input to retrieve resources. However, in this case, the input is the object ID itself. The vulnerable API endpoint receives an object ID provided by an authenticated user and tries to access it.
A good example of this attack would be the following:
/* securefirm.com/secureapi/user/download_record/54321 */
app.get("/securefirm.com/secureapi/user/download_record/:record_id", authenticateToken, (req, res) => {
const { id } = req.params;
const record = db.records.filter((a) => {
return a.id === id;
})[0];
res.json(record);
});
Although convenient and easy to implement, you should avoid this kind of practice at all costs, given the potential for exploitation.
Some could reason that there are particular circumstances where data is not sensitive, and you could forgive the use of more simple, lenient methods. However, I would argue that this vulnerability is, in fact, representative of a fundamental gap in the infrastructure and design of any system, and accepting the gap as a feature is shortsighted and dangerous.
Addressing NodeJS Broken Object-Level Authorization
As grim and scary as all this might seem, I have some good news. The mitigation strategies to address these vulnerabilities are pretty simple. However, depending on the scale of your platform, you might need to do a lot of testing.
For brevity's sake, we will provide a simple session-based method to provide an authorization mechanism in NodeJS, but keep in mind that you can use more robust solutions like passport and Auth0.
If you already have an authentication mechanism, you can skip until the last step of this section.
First, make sure you have installed the following packages:
npm install express express-session mongoose connect-mongo
We will use express-session to handle the session creation and MongoDB to store the session in the database.
Next, modify your application code to include the libraries just loaded.
const express = require('express');
const app = express();
const mongoose = require('mongoose');
const MongoStore = require('connect-mongo');
const session = require('express-session');
await mongoose.connect('MONGO_URL', (err, db) => {
console.log('---DATABASE ONLINE---');
});
app.use(session({
secret: 'YOUR_SECRET_KEY',
resave: false,
saveUninitialized: true,
cookie: { maxAge: oneDay },
store: MongoStore.create({
mongoUrl: 'MONGO_URL'
})
}));
.... YOUR CODE HERE ....
Then, proceed to add a route with the login form if you don't already have one.
app.post('/login', async (req, res) => {
const { usr, pwd } = req.body;
try {
let user = await User.findOne({ email: usr })
req.session.userId = user.id;
console.log(req.session);
res.redirect('/')
} catch (err) {
console.log(err);
res.json({ msg: 'Server Error' });
}
})
Finally, add a middleware that will add the session data to the request header and retrieve it on every request for validation purposes.
module.exports.authentication = async (req, res, next) => {
const uId = req.session.userId;
if (!uId) {
return res.redirect('/login?q=session-expired');
}
try {
let user = await User.findById(uId);
if (!user) {
return res.redirect('/login?q=session-expired');
}
next();
} catch (err) {
console.log(err);
res.json({ msg: 'Server error' })
}
};
Now that we have a way to identify the user, let's implement some mitigation strategies in our project.
Vulnerable User ID Endpoints
To address this vulnerability, ensure that the user ID in the session object, which resides in req.session.userId, matches the user-provided value. If there is no match, then deny access and redirect the user. That's it.
Vulnerable Object ID Endpoints
Addressing this vulnerability works similarly. You need to confirm that the user has access to this object by including it in the query string that retrieves it.
One example would be the following:
app.get("/securefirm.com/secureapi/user/download_record/:record_id", authenticateToken, (req, res) => {
const { id } = req.params;
const { user_id } = req.session.userId;
const record = db.records.filter((a) => {
return a.id == id && a.user_id == id;
})[0];
res.json(record);
});
Moving Forward
One of the main reasons that broken object-level authorization vulnerabilities are so rampant and ubiquitous is poor authorization mechanisms and human oversights.
However, it is essential to remember that you are responsible for protecting your user data and implementing robust solutions available.
We recommend you consider using our dynamic application security testing (DAST) at StackHawk.
DAST runs security tests against a running application in real time, finding vulnerabilities your team introduced and exploitable open source vulnerabilities like broken object-level authorization.
You can learn more about it here.
This post was written by Juan Reyes. Juan is an engineer by profession and a dreamer by heart who crossed the seas to reach Japan following the promise of opportunity and challenge. While trying to find himself and build a meaningful life in the east, Juan borrows wisdom from his experiences as an entrepreneur, artist, hustler, father figure, husband, and friend to start writing about passion, meaning, self-development, leadership, relationships, and mental health. His many years of struggle and self-discovery have inspired him and drive to embark on a journey for wisdom.