For developers striving to build secure APIs, Insecure Direct Object References (IDOR) stand out as a critical vulnerability to look for. As outlined in the OWASP API Security Top 10, this vulnerability can lead to unauthorized access and manipulation of data, leading to disastrous consequences. Broken Object Level Authorization (BOLA) vulnerabilities arise when applications fail to enforce adequate authorization checks for accessing objects or resources. This flaw enables attackers to exploit user-supplied inputs, like URL parameters, to access or manipulate objects they shouldn't, such as files or database records. Without proper authorization validation, attackers can bypass restrictions, potentially leading to unauthorized data access and system breaches.
In this blog, we will examine the details surrounding BOLA, including what it is, the root causes of BOLA vulnerabilities, and how to identify BOLA vulnerabilities in your API code. We will then use StackHawk to scan a vulnerable Node application and detect a BOLA vulnerability in one of the endpoints. Lastly, we will show the steps required to fix the vulnerability and retest the code to ensure it has been resolved. Let’s start by taking a deeper look at what BOLA is.
What is Broken Object Level Authorization (BOLA)?
Broken Object Level Authorization (BOLA), previously known as Insecure Direct Object Reference (IDOR), remains a top threat to API security. This vulnerability emerges when an API lacks rigorous controls, preventing users from accessing resources or data outside their intended permissions.
Object-level authorization defines an application's rules for what actions users can take on specific pieces of data. A healthcare app, for example, should limit patient access to only their medical records. BOLA vulnerabilities arise when these controls are inadequate or absent. This might include an API using easily guessable object identifiers (think customer IDs) or not verifying if a user's request aligns with their actual permissions.
BOLA attacks tend to follow a simple pattern. The attacker first studies API traffic to identify how objects are referenced (e.g., /users/{userID} or /orders/{orderID}). They'll then try to modify these identifiers, hoping to reach data belonging to unauthorized users. An attacker could view, change, or even delete sensitive information if authorization checks are weak. In severe cases, complete and unauthorized control over objects might be possible.
BOLA's prevalence and danger come from its ease of exploit (attackers don't always need deep technical knowledge) and the difficulty traditional security scanners have identifying these flaws. The fallout of a successful BOLA attack can include privacy violations, compromised accounts, financial theft, or even disruption of API-controlled systems.
What is The Difference Between BOLA and Insecure Direct Object Reference (IDOR)?
You may come across both "BOLA" and "IDOR" when discussing this type of API vulnerability. While there's a lot of overlap between the terms, understanding their subtle distinction is important. Insecure Direct Object Reference (IDOR) is the older phrasing, highlighting situations where an API reveals internal object references or identifiers that are easy for an attacker to manipulate. Imagine a URL structure like /api/customers/{customerID}; an attacker could try substituting different customerIDs to access data they shouldn't.
The shift to the term Broken Object Level Authorization (BOLA) reflects a broader understanding of the problem. The core issue isn't solely about the type of reference used but rather a breakdown in authorization logic. This could mean the API relies on predictable object identifiers or fails to thoroughly check if the user making a request has legitimate ownership or permissions to interact with the object in question.
To recap, IDOR vulnerabilities all fall under the umbrella of BOLA. However, not every BOLA vulnerability necessarily involves a directly exposed, easily manipulated object reference. The fundamental problem always centers around the API incorrectly implementing object-level authorization checks.
What is The Root Cause of BOLA?
BOLA vulnerabilities arise from development practices prioritizing ease of use over building strong authorization into the API's logic. Here's a breakdown of the main reasons this happens:
Over-reliance on Object Identifiers
When APIs directly reference objects using easily guessed patterns (sequential numbers, predictable text, etc.), it's an open invitation for attackers. A URL like /api/documents/{documentID} makes it tempting for attackers to try different documentIDs and potentially find things they shouldn't.
Lack of Ownership and Permission Verification
APIs must consistently check that the user making the request owns the object they're targeting or has the right permissions to work with it. This can't be a one-time check; it needs to be done with each request.
Blind Trust in User Input
Anything sent to the API by a user, including object identifiers, must be treated with caution. Thorough input validation is also necessary to prevent attackers from sneaking in values designed to break API logic.
Insufficient Focus on Authorization
Development pressure can lead to inconsistent or overlooked authorization for some API actions. Strong authorization must be baked into the design.
The Fallacy of Obscurity
Using hard-to-guess identifiers ( like long UUIDs ) can create a false sense of security. Attackers are resourceful; if the only protection is a 'tricky' identifier, they'll likely find a way around it.
Ultimately, BOLA comes down to not building strong, object-level authorization checks directly into the API's code. Convenience and 'hiding' things can never replace a well-designed authorization system.
Preventing Broken Object Level Authorization (BOLA) Vulnerabilities
Since BOLA vulnerabilities happen when an application doesn't have strong enough checks before allowing access to objects. To combat this, you need a thorough approach to building and maintaining your APIs, emphasizing secure design, robust authorization, and constant vigilance. Here are a few ways to effectively reduce your BOLA risk:
Core Practices
Enforce Strong Authorization Checks: Every time a user tries to do something, verify they have the right permissions. This has to be baked into the heart of your APIs.
Consider Attribute-Based Access Control (ABAC): While role-based systems (RBAC) work well in many cases, ABAC lets you be more specific. ABAC policies can consider the user, the target object, and the action – ideal for complex applications with many moving parts.
Use Scoped Access Tokens: If you use tokens (like OAuth or JWTs), limit what those tokens can access, such as permission to only perform specific CRUD operations. This lessens the impact if a token is stolen or misused.
Proactive Measures
Regular Security Audits and Penetration Testing: Use outside experts to thoroughly test your application and platforms like StackHawk, which can actively test for vulnerabilities like BOLA. Using these teams and tools may find BOLA vulnerabilities your internal teams missed.
Educate Developers: Teach your development team how to code with security in mind and how to spot common vulnerabilities like BOLA.
Use Secure Frameworks and Libraries: Tools that encourage secure practices and include pre-built authorization features can save time and reduce errors.
Adopt a Zero Trust Mindset: Never assume a request is safe, even if it seems to be from a legitimate user inside your network. Verify the authorization of every request.
By following these guidelines, you'll significantly reduce your application's risk of BOLA vulnerabilities, ensuring users can only access what they're supposed to. Now, let’s put this into practice by looking at a vulnerable NodeJS application and use StackHawk to identify the BOLA vulnerability and help us verify that the fix we will implement successfully remediates the issue.
Detecting and Fixing BOLA Vulnerabilities in NodeJS
Building The Vulnerable Endpoint
Below is an example of a simple Node.js API with a BOLA vulnerability. This API allows users to access user data based on a user ID passed in the URL. However, it doesn't perform any checks to ensure that the user making the request can access or modify the data for that user ID. This demonstrates a BOLA vulnerability in its most simple form.
Please note: This is an intentionally vulnerable API for demonstration purposes. Do not use this code in a real application as it is not production-ready.
To run this app, first, ensure you have Node.js installed. Then, create a new directory for your project, ours is named “bola-api-example”, and initialize the directory by running
npm init
You'll also need to install Express, a popular web framework for Node.js, as well as a few other dependencies we will use in the project. You’ll do this by running the following npm command:
npm install express body-parser sqlite3 express-oas-generator jsonwebtoken
Note: The jsonwebtoken dependency will be used later, but we will install it now for ease.
Now, in the root of the project we just created, create a file named app.js and add the following code:
const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const expressOasGenerator = require('express-oas-generator');
// Initialize Express app
const app = express();
expressOasGenerator.init(app,
function(spec) {
// Modify the spec here
if (spec.paths['/users/{id}']) {
let path = spec.paths['/users/{id}'];
if (path.get) {
path.get.parameters = [{
in: 'path',
name: 'id',
required: true,
schema: {
type: 'integer'
},
description: 'User ID to fetch'
}];
}
}
return spec;
});
app.use(bodyParser.json());
// Initialize SQLite database
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
db.run("CREATE TABLE users (id INT, name TEXT)");
// Insert example users
const stmt = db.prepare("INSERT INTO users (id, name) VALUES (?, ?)");
stmt.run(1, "John Doe");
stmt.run(2, "Jane Smith");
stmt.run(3, "Alice Johnson");
stmt.finalize();
});
// Secure SQL query endpoint using parameterized query
app.get('/users/:id', (req, res) => {
let userId = req.params.id;
let sql = "SELECT * FROM users WHERE id = ?";
db.all(sql, [userId], (err, rows) => {
if (err) {
res.status(500).send("Error in database operation");
} else {
res.json(rows);
}
});
});
// Start server
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Let’s run through a quick breakdown of the code above that shows how to create a simple web server using Node.js, Express.js, SQLite3, and automated OpenAPI documentation creation with express-oas-generator
. The code provides a REST API for accessing user information stored in an SQLite database, all running in memory for simplicity and demonstration purposes. Here's a breakdown of the key components and actions within the code:
At the beginning (const app = express();
), an Express application is initialized, which serves as the backbone for creating API endpoints. Then, we create the configuration for OpenAPI Documentation with express-oas-generator
. The expressOasGenerator.init(app, ...);
section initializes the automatic generation of OpenAPI documentation for the Express app. This documentation is dynamically adjusted based on the defined API paths. In this case, we also explicitly modify the API documentation to detail the parameters expected by the /users/{id} endpoint.
Next, we configure the Express app to use body-parser middleware, allowing the app to parse JSON request bodies. This is essential for endpoints that accept data (e.g., POST requests), although we don’t directly use it in the code since we only have a GET endpoint defined.
After this, The code sets up an SQLite database in memory (const db = new sqlite3.Database(':memory:');
). This in-memory database is temporary and ideal for demonstration, as it doesn't require us to connect to an external database engine. Within db.serialize(...)
, a table named users is created, and three example user records are inserted into this table. This sets up the initial data state with which the API will interact.
Then we define the API Endpoint to Fetch User Data. The route app.get('/users/:id', ...)
defines an API endpoint that allows clients to fetch user data by ID using a parameterized SQL query to prevent SQL injection.
Finally, we initialize the application, which listens to incoming requests on a specified port (
const PORT = 4000;
)
. This makes the API accessible to clients, and a console log message confirms the server's successful launch.
Running The Code
Now that our code is complete, it’s time to run the application and bring up our API. To run your API, run the following command in a terminal pointed to the root of your project:
node app.js
Once the application is up and running, you’ll notice that the GET /user/:id endpoint allows anyone to retrieve user data by simply specifying a user ID in the URL.No authentication or authorization checks are performed, meaning any user can access or modify any other user's data. This is a classic example of BOLA vulnerability.
Detecting The Vulnerability With StackHawk and HawkScan
Now that our code is set up and our API is running, let’s get StackHawk to identify this vulnerability for us automatically. To do this, you’ll need to ensure you have a StackHawk account. If you need one, you can sign up for a trial account or log into an existing account.
If you’re logging into an existing StackHawk account, from the applications page, you’ll click Add an App -> Create Custom App
If you’re new to StackHawk, you’ll be automatically brought into the Add an App flow. On the Scanner screen, you’ll see the instructions for installing the StackHawk CLI. Since we will be running our testing locally, we will use this option. Once the hawk init command is executed successfully, click the Next button.
On the next screen, you will fill out an Application Name, Environment Name, and Host. Feel free to copy the default values I’ve added in the screenshot below for our purposes. Once filled out, click Next.
Since we will be testing a RESTful API, on the next page, we will choose our Application Type as “API”, the API Type as “REST / OpenAPI”, and point to our OpenAPI Specification file by selecting URL Path and adding in our endpoint, /api-spec/, into the textbox. Once complete, click Next.
Lastly, we will need to add a stackhawk.yml file to the root of our project. Once the file is added, copy the screen's contents, paste it into the file, and save it. Lastly, we can click the Finish button.
Enabling BOLA Detection in HawkScan
After creating the app in StackHawk, we need to do a few more steps to enable BOLA (and IDOR) detection in StackHawk. Two ways to do this are using the "OpenAPI - Experimental" policy or customizing an existing policy. The "OpenAPI - Experimental" policy allows users to scan for the vulnerabilities outlined in the OWASP API Security Top 10.
To set HawkScan up with the "OpenAPI - Experimental" policy, you will log into StackHawk, go to the Applications screen, and click on the settings link for your application.
Next, on the Settings screen, under HawkScan Settings, click the Applied Policy dropdown and select the "OpenAPI - Experimental" policy.
At this point, you can then run HawkScan and begin detecting potential BOLA vulnerabilities. Of course, another way to do this is to simply customize an existing policy to scan for BOLA/IDOR vulnerabilities. To show how this can be done, we can navigate to the same HawkScan Settings section that we looked at above. From here, we will select the "OpenAPI/REST API" policy from the Applied Policy dropdown.
Next, click the Edit Policy button right beside the dropdown.
On the next screen, under the Plugins and Tests section, click on the BOLA entry to enable HawkScan to execute tests for potential BOLA vulnerabilities.
If you also want to enable IDOR testing, you can click on the Passive Scan Plugins tab and then select IDOR.
Now, with our Application set up in StackHawk, our stackhawk.xml added to the root of our API project, and BOLA/IDOR detection enabled, we can finally test our application.
Run HawkScan
In order to test our application, in a terminal pointing to the root of our project, we will run HawkScan using the following command:
hawk scan
After running the command, the tests should begin to execute in the terminal.
This will run tests against our API based on the OpenAPI spec using the OpenAPI Spider provided by StackHawk.
Explore The Initial Findings
Once the tests are complete, the terminal will contain some information about any vulnerabilities found. Below, we can see that it has identified the BOLA vulnerability that we introduced in the code. To explore this further, we will click on the test link at the bottom of the output. This will take us into the StackHawk platform to explore further.
After clicking on the link, we can now see the test results nicely formatted. Next, we will click on the Possible Broken Object-Level Authorization (BOLA) entry.
Within this entry, we can see an Overview and Resources that can help us with fixing this vulnerability as well as the Request and Response that the API returned. Above this, you will also see a Validate button, which will display a cURL command with the exact API request used to expose the vulnerability.
Fixing The BOLA Vulnerability
To fix the BOLA vulnerability in the Node.js API, you must implement authentication and authorization checks. This will ensure that users can only access and modify their own data. For simplicity, I'll demonstrate a basic version of these checks. In a real-world application, you'd likely use more robust authentication and authorization mechanisms and platforms, such as Auth0 or Okta, to control access and handle credential creation.
To remedy our BOLA vulnerability, update your app.js with the following code:
const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const expressOasGenerator = require('express-oas-generator');
const jwt = require('jsonwebtoken');
const app = express();
const JWT_SECRET = 'your_jwt_secret_here';
expressOasGenerator.init(app,
function(spec) {
if (spec.paths['/users/{id}']) {
let path = spec.paths['/users/{id}'];
if (path.get) {
path.get.parameters = [{
in: 'path',
name: 'id',
required: true,
schema: {
type: 'integer'
},
description: 'User ID to fetch'
}];
}
}
return spec;
});
app.use(bodyParser.json());
// Initialize SQLite database
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
db.run("CREATE TABLE users (id INT, name TEXT, role TEXT)");
// Insert example users with roles
const stmt = db.prepare("INSERT INTO users (id, name, role) VALUES (?, ?, ?)");
stmt.run(1, "John Doe", "admin");
stmt.run(2, "Jane Smith", "user");
stmt.run(3, "Alice Johnson", "user");
stmt.finalize();
});
// JWT authentication middleware as a function
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) {
return res.sendStatus(401); // If no token, return 401
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.sendStatus(403); // If token is not valid, return 403
}
req.user = user;
next();
});
};
// Authorization middleware to check if the user can access the data
function authorize(req, res, next) {
const userId = parseInt(req.params.id);
if (req.user.sub !== userId) {
return res.status(403).send("Not authorized to access this resource");
}
next();
}
// Secure SQL query endpoint using parameterized query with authorization
app.get('/users/:id', [authenticateJWT, authorize], (req, res) => {
let userId = req.params.id;
let sql = "SELECT id, name FROM users WHERE id = ?";
db.all(sql, [userId], (err, rows) => {
if (err) {
res.status(500).send("Error in database operation");
} else {
res.json(rows);
}
});
});
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
The updated code above introduces several significant enhancements and changes compared to the original version, specifically focusing on security through the use of JSON Web Tokens (JWT) for authentication and authorization to eliminate the BOLA vulnerability.
The updated code introduces the jsonwebtoken package to implement JWT-based authentication and authorization. This is a significant enhancement over the original code, which did not include any form of user authentication or authorization. To use this, you’ll also see the addition of the constant JWT_SECRET defined, which is used to sign and verify JWTs. This key is essential for the security of the token-based system, ensuring that tokens are valid and issued by the server. Of course, the key is extremely simple since it is just for demonstration purposes. In a production system, you’ll want to have a longer and complex key that is not directly defined in the code!
The updated code also defines a method called authenticateJWT, a middleware function that checks for the presence of a JWT in the Authorization header of incoming requests. It verifies the token using the JWT_SECRET. If the token is missing or invalid, it sends a 401 or 403 response, effectively securing the endpoint from unauthorized access.
Next, the authorize middleware function is defined which checks if the authenticated user (decoded from the JWT) is authorized to access the requested user data. The comparison is made using the sub claim from the JWT against the user ID from the request parameters. The /users/:id endpoint is now protected by both the authenticateJWT and authorize middleware, arranged in an array. This sequential middleware arrangement ensures that every request to this endpoint is first authenticated and then authorized, a common pattern to enforce security on API endpoints.
Once the code has been updated on your side, you can stop the currently running server (by using ctrl + C in the terminal) and restart it with the new code using:
node app.js
The app should now be up and running with the updated code!
Confirm the fix!
With the latest code, users must send a JWT along with their request in the Authorization header. The code will then extract the sub field to ensure that the sub value (the user's ID) matches the ID of the record they are trying to access. In the JWT.io debugger, this is how such a token would look:
Now, let’s confirm the fix in StackHawk. To do this, we will click the Rescan Findings button back in StackHawk.
Then, we will see a modal containing the “hawk rescan” command that includes the correct Scan ID. You’ll run this command in the same terminal where you ran the initial set of tests.
In the output, you will again see any vulnerabilities found in the scan. In this case, you’ll see that the BOLA vulnerability is no longer showing. Clicking on the link at the bottom of the terminal output, you can confirm that the BOLA vulnerability has now been added to the Fixed Findings from Previous Scan, confirming that the vulnerability has been successfully fixed and has passed any vulnerability tests.
With that, we’ve successfully remedied and retested our API to ensure its safety from potential BOLA attacks. Please remember that the code above is for demonstration purposes and that adding robust, production-grade authorization to an API endpoint is much more involved than we implemented above. Using an API gateway and identity provider, such as Auth0, is one of the best ways to prevent BOLA and secure your APIs from unauthorized use.
Why StackHawk?
When it comes to preventing vulnerabilities, such as BOLA, StackHawk offers a comprehensive platform for developers to secure their applications and APIs. StackHawk is a modern, powerful DAST tool designed for developers to integrate application security testing seamlessly into their software development lifecycle. It is not just another security tool; it's a developer-first platform emphasizing automation, ease of use, and integration into existing development workflows.
At its core, StackHawk is built around empowering developers to take the lead in application security. It provides a suite of tools that make it easy to find, understand, and fix security vulnerabilities before they make it to production. One of the critical components of StackHawk is HawkScan, a dynamic application security testing (DAST) tool that scans running applications and APIs to identify security vulnerabilities.
With StackHawk, developers and security teams can:
Automate Security Testing: Integrate security testing into CI/CD pipelines, ensuring every build is automatically scanned for vulnerabilities.
Identify and Fix Vulnerabilities: Receive detailed, actionable information about each vulnerability, including where it is in the code and how to fix it.
Test in All Environments: Use StackHawk in various environments, including development, staging, and production, to catch security issues at any stage of the development process.
Customize Scans: Tailor scanning configurations to suit specific applications and environments, ensuring more relevant and accurate results.
By focusing on a developer-first approach to security testing and integrating smoothly into existing dev tools and practices, StackHawk demystifies application security, making it a more approachable and manageable part of the software development lifecycle.
Conclusion
In this blog, we delved into the complexities of Broken Object Level Authorization (BOLA) vulnerabilities. We unpacked the essence of BOLA, revealing how it emerges from insufficient authorization checks, allowing attackers to manipulate or access data beyond their permissions. The discussion illuminated the root causes, from over-reliance on predictable object identifiers to the lack of validation and authorization in API requests. We looked into understanding BOLA's mechanics and the significance of adopting a defense strategy encompassing strong authorization checks, ABAC, and a zero-trust approach, among other security best practices.
From a practical perspective, we looked at how to identify a BOLA vulnerability within a NodeJS application using StackHawk, a cutting-edge Dynamic Application Security Testing (DAST) tool designed for developers. By integrating security testing directly into the development workflow, StackHawk pinpointed the BOLA vulnerability and provided actionable insights to remediate it effectively. Through a simple example, we showcased the power of adding authentication and authorization middleware to secure API endpoints against unauthorized access, thus mitigating the BOLA threat we introduced in the original code.
Embracing StackHawk in your development cycle equips your team with the necessary tools and insights to tackle security vulnerabilities, including BOLA, preemptively. This approach not only enhances the security posture of your applications but also fosters a culture of security mindfulness among developers. Experience firsthand how StackHawk's seamless integration and developer-centric solutions can transform your application security testing workflow, ensuring your applications are robust against common API threats outlined in the OWASP API Security Top 10. Sign up for a free trial of StackHawk today and join the ranks of proactive developers who are “shifting left” and making API security an integral part of their development process with StackHawk.