Skip to main content

2 posts tagged with "guides"

View All Tags

ยท 8 min read
Karan Kajla

Insecure Direct Object Reference (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?โ€‹

IDOR 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:

User ID 2's GET Request
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:

User ID 2's PUT Request
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).

User ID 81's PUT Request
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 Discord, Twitter, and Reddit to hear more about IDOR, access control, and everything security related.

ยท 7 min read
Karan Kajla

Access Control is the process of allowing (or disallowing) user access to specific resources or actions in a software system. For example, only allowing certain users access to internal admin pages on a website or only allowing paying users access to a premium feature. There are many approaches to implementing Access Control, but Role Based Access Control (RBAC) is one of the most popular and widely used. In this guide, we'll cover a standard way to implement RBAC and discuss some best practices for implementing Access Control in APIs and web applications.

Overviewโ€‹

RBAC is an Access Control model in which the ability to access a resource or action in a system is tied to a permission (or policy), and each of those permissions is associated with one or more roles. Every user in the system has one or more roles and thus has all the permissions associated with those roles. This hierarchical structure is fairly intuitive and works really well for managing user access in a wide range of applications such as SaaS & enterprise software, e-commerce websites, company internal apps, and more.

Since Access Control (especially RBAC) is fundamentally a relational problem, it makes sense to use a relational database like PostgreSQL or MySQL to implement it. We'll break down the data model in this guide using SQL.

Usersโ€‹

Before doing anything else, we need to keep track of users in our application. For the purpose of this guide, we just need a unique id for each user in our system. This id will be used later to associate users with roles. Most applications will also store extra user information like an email, a password, and first & last name.

CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(255),
PRIMARY KEY (id)
);

Permissionsโ€‹

Once we have users, the first step in implementing RBAC is to define a set of permissions (or policies) and associate each permission with a privileged action in your application. For example, you might define a user:create permission associated with the ability to create a new user in your application. Only users that have the user:create permission will be able create a new user.

To manage permissions, we really only need each permission to have a unique id like users have. This is enough to associate permissions to roles. To make working with permissions more human-friendly, we'll make this id a unique string identifier (i.e. something like user:create) instead of an integer. This will make it easy to understand what each permission represents just by looking at its id.

CREATE TABLE permissions(
id VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);

Rolesโ€‹

Once we have our permissions defined, it's time to group them together in the form of roles. Roles are like personas for the different types of users that access our application. These personas will dictate which permissions should be grouped together for each role. For example, if your application has a free tier with basic features that are available to all users and a premium tier with more powerful features available only to paying users, you might define two different roles: One role called free_tier_user that grants access to all of the basic features and another role called premium_user that grants access to both the basic and premium features.

The data model for roles will look exactly like the model for permissions.

CREATE TABLE roles(
id VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);

Role Permissionsโ€‹

Since every role will have permissions, we need a way to associate permissions to roles. We'll call this relationship a role permission. To represent a role permission, we need to track the id of the role and the id of the permission we're associating together. Note that permissions can belong to multiple roles.

CREATE TABLE role_permissions(
role_id VARCHAR(255) NOT NULL,
permission_id VARCHAR(255) NOT NULL,
PRIMARY KEY (role_id, permission_id)
);

User Rolesโ€‹

Since users will have roles, we also need a way to associate users to roles. We'll call this relationship a user role. To represent a user role, we need to track the id of the role and the id of the user we're associating together. Note that users can have multiple roles.

CREATE TABLE user_roles(
role_id VARCHAR(255) NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY (role_id, user_id)
);

Authorizationโ€‹

Now that we have a data model to store and associate users, roles, and permissions, we can use permissions to protect access to resources and actions in our system. We need (1) a way to check the data model to figure out if a user has a permission and (2) an easy way to perform these checks anywhere in our application code. This process of validating user access is known as authorization.

Querying Permissionsโ€‹

We can figure out if a user has a particular permission by querying our data for a relation between the user attempting to gain access and the permission required to gain access.

# Given a user id 15 and a permission id users:create,
# we can determine if the user has the required permission by:
# (1) Getting the user's role(s)
# (2) Checking if any of the user's roles grant them the required permission
SELECT *
FROM permissions
INNER JOIN role_permissions
ON permissions.id = role_permissions.permission_id
WHERE
permissions.id = "users:create" AND
role_permissions.role_id IN (
SELECT role_id
FROM roles
WHERE user_id = 15
);

The query above will only return a result if the user with id 15 has the users:create permission through one of their roles. If not, it will return an empty result, meaning the user does not have the required permission through any of their roles.

Access Checks in Codeโ€‹

To make it easy to check for a permission anywhere in your application, you might abstract the query above into a helper class or method that can be called anywhere in your code. In JavaScript, this might look like:

class Authorization {
// Returns true if the user has the
// permission with the given permissionId
static function hasPermission(userId, permissionId) {
// NOTE: Assume getPermissionsForUser runs the SQL query from above
const permissions = getPermissionsForUser(userId);

return permissions.length > 0;
}
}

You can then call this helper method to protect actions in your application:

function createUser(newUser) {
const currentUserId = UserSession.getCurrentUserId();

if (!Authorization.hasPermission(currentUserId, "users:create")) {
throw new Error("Unauthorized attempt to create a new user!");
}

//
// logic to create a new user
//
}

Best Practicesโ€‹

Well-implemented Access Control is one of the most effective ways to provide data privacy for users and prevent potential data loss or theft. When implementing an Access Control model like RBAC, it's important to keep a few things in mind:

  • Define permissions based on resources and actions (i.e. users:create). This leads to well-defined permissions that never need to change even if your roles and application logic do.

  • Do not perform access checks based on role. This can be a very rigid approach that makes it hard to change your access model without updating code. Instead, it's better to check for access to permissions since these map directly to the resources and actions in your application.

  • Have a centralized access control service. Since access control is independent of your application's business logic, it's a good idea to separate authorization logic into it's own service. We did this with the Authorization class we implemented in JavaScript above.

Access Control isn't a core focus for most applications, but it's critical to get right. The margin for error is very low, and even a minor issue in authorization logic could expose privileged data and actions to users who shouldn't have access to them. If you don't want to worry about authorization best practices and implementing secure access control, use Warrant to add access control to your application using RBAC or other access models in under 20 lines of code.