Insecure Direct Object Reference & How to Protect Against it
Insecure Direct Object Reference (shortened as IDOR) is one of the most common forms of broken access control which OWASP recently listed as the number one application security issue in 2021. A quick search for "IDOR" on Hacker One's Hacktivity feed shows that many top tech companies (and even the U.S. Department of Defense) have fallen victim to IDOR, in some cases paying out well over $10,000 per bug bounty. In this post, I'll explain what IDOR is, what causes it, and ways to protect your application against it.
What is Insecure Direct Object Reference?
Insecure Direct Object Reference is an access control vulnerability that occurs when an application uses an identifier for direct access to an object in a database but doesn't perform proper access control or authorization checks before querying or modifying the database. This often results in vulnerabilities that allow malicious users to access or modify other objects in the application, including those belonging to other users.
To better understand what IDOR is, let's walk through an example. A common case of IDOR occurs when developers implement the ability to update objects in their application. Typically, an update operation exposes the internal identifier of the object to the user (in the URL or API endpoint, for example), and that identifier is used by the system to update the appropriate object.
Consider an API for managing objects in a SQL database with the following table structure:
# NOTE: each object belongs to a single user
CREATE TABLE objects(
id INT NOT NULL AUTO_INCREMENT,
userId INT NOT NULL,
name VARCHAR(255),
description VARCHAR(255),
PRIMARY KEY (id)
);
We have a GET /objects/:objectId
endpoint for retrieving individual objects:
GET https://api.my-app.com/v1/objects/2408
Which returns:
{
"id": 2408,
"name": "My Object",
"userId": 2,
"description": "This is my object."
}
We also have a PUT /objects/:objectId
endpoint for updating individual objects:
PUT https://api.my-app.com/v1/objects/2408
Which takes a request body:
{
"id": 2408,
"name": "My Object",
"description": "My object's updated description."
}
In our API, the code for our PUT endpoint simply parses the request body and uses it to generate and execute a SQL UPDATE
query for the object specified in the request body:
UPDATE objects
SET
name = "My Object"
description = "My object's updated description."
WHERE id = 2408;
The key thing to note is our code doesn't explicitly check that the object being updated belongs to the user making the request. The query above also doesn't have a condition checking for ownership (i.e. WHERE userId = <logged_in_user_id>
).
As a result, a malicious user (userId 81
for example) could make a request to our PUT endpoint and update any user's objects (provided they specify a valid id).
PUT https://api.my-app.com/v1/objects/2408
{
"id": 2408,
"name": "User 81's Object",
"description": "This object has been modified by user 81."
}
This issue seems fairly benign when dealing with example objects, but can become a major concern when malicious users can modify someone else's banking details or fetch another user's billing information.
Mitigation Techniques
Preventing IDOR in an application requires consistent attention to detail, and there isn't really one silver bullet solution to fix it once-and-for-all (especially in a growing application). However, there are many ways to mitigate it, and I'll cover a few of them below. The best way to keep an application free from vulnerabilities related to IDOR is to employ a combination of the methods described, especially the last one.
Use Non-Contiguous Identifiers
Even in a system with IDOR issues, a malicious user still needs to be able to enumerate or guess valid identifiers for other objects in order to access or modify them. In the example IDOR scenario from above, if the malicious user 81
didn't know the id of user 2
's object (2408
), they wouldn't be able to modify it. However, since we use a standard SQL integer identifier that increments by 1 each time a new record is created, it's pretty easy to guess valid identifiers. One way to prevent this is to use non-contiguous identifiers for database objects (like a UUID). However, this strategy does come with its downsides (UUIDs take more space to store, database index performance can suffer due to non-contiguous identifiers, etc.) and isn't guaranteed to prevent exploits as a result of IDOR.
Require Ownership Conditions in Database Queries
There are multiple layers in an application at which ownership or access control checks can be added to prevent IDOR. At the very least, requiring these checks at the database layer (the lowest layer in most web apps/services) can significantly reduce IDOR issues. Whether you're using an ORM or writing your own queries, a good way to enforce that all database queries include ownership checks is to abstract out all database interactions into separate "Service" and "Repository" classes (learn more about this pattern here).
The repository class is responsible for taking inputs and executing database queries (always written with an ownership check) based on those inputs, while the service class acts as a higher level interface to the repository you can use throughout your code. Following this pattern, all database queries for a particular database table are isolated to a single class/file which makes it easier to monitor and enforce ownership checks in queries. More importantly, any code using the service class to interact with the data is required to provide an owner id. Here's a basic example in JavaScript:
class ObjectRepository {
get(userId, objectId) {
/*
* Execute the following SQL statement via ORM, etc.
*
* SELECT *
* FROM objects
* WHERE
* userId = <userId> AND
* id = <objectId>;
*/
}
update(userId, object) {
/*
* Execute the following SQL statement via ORM, etc.
*
* UPDATE objects
* SET
* name = <object.name>
* description = <object.description>
* WHERE
* userId = <userId> AND
* id = <object.id>;
*/
}
}
class ObjectService {
constructor(userId) {
if (!userId) {
throw new Error("Missing required parameter: userId");
}
this.userId = userId;
this.repository = new ObjectRepository();
}
getById(objectId) {
const object = this.repository.get(this.userId, objectId);
if (!object) {
throw new Error("Not Found");
}
return object;
}
update(object) {
const updatedObject = this.repository.update(this.userId, object);
if (!object) {
throw new Error("Not Found");
}
return updatedObject;
}
}
//
// Example Usage of ObjectService
//
const objectService = new ObjectService(UserSession.getUserId());
try {
const updatedObject = objectService.updateObject(request.body);
response.json(updatedObject);
} catch (e) {
response.sendStatus(404);
}
Using patterns like the one above to enforce that database queries include ownership conditions will go a long way in preventing major IDOR vulnerabilities. However, not all ownership schemes map directly to a simple query condition like in our example. Take a collaborative text editing application like Google Docs, a file sharing service like Dropbox, or even an enterprise software service where users can grant other users the ability to view and edit documents on a per object basis. Applications like these typically maintain a set of ownership rules to help determine who has access to objects and require much more in order to prevent IDOR.
Implement Proper Authorization & Access Control
The most effective way to prevent IDOR in applications is to implement a proper authorization & access control system. One that can be queried anywhere in an application to determine if a user has the appropriate access to an object or resource before allowing them access to it. You can then add access control checks to your application wherever privileged data is accessed or modified, much like we did in our example above. To learn how to implement a basic Role Based Access Control system, check out our previous write-up on Implementing Role Based Access Control in a Web Application.
A good access control system should be able to model your application's database schema & how all your data is related, allow you to implement common access control schemes (like Role Based Access Control, Fine Grained Access Control, etc.), and provide an easy way to check against dynamic access control rules in your code to prevent vulnerabilities like IDOR. It should also be flexible, allowing you to implement whatever access control model your application requires, from basic RBAC to more complex models used for services like Google Docs or Dropbox. Finally, in a good access control system, the ability to modify your access model without having to push code changes should be a requirement.
Building secure, flexible, and consistent access control for an applicaton is difficult and often not the focus of most developers building software. Warrant is an API-first access control service that makes it easy for any developer to drop a few lines of code into their application and instantly have secure access control. We have a generous free tier so you can test it out in your next application. Get started today!
Join the discussion on Slack, Twitter, and Reddit to hear more about IDOR, access control, and everything security related.
Get started with Warrant
Warrant is a developer platform for implementing authorization in both customer-facing and internal applications. Build robust RBAC, ABAC, and ReBAC for your company or product with Warrant's easy-to-use API and SDKs.
Start BuildingRead the Docs