OWASP TOP 10 API Security Part 2 (Broken Object Level Authorization)
In this article we will explore the first of the OWASP Top 10 API security risks for year 2019. (API1:2019 - Broken object level authorization).
Join the DZone community and get the full member experience.
Join For FreeIn the previous article, we had them build our ground around OWASP's top ten projects and covered the brief official definition of OWASP Top 10 for API security.
In this article, we will explore the first of the OWASP Top 10 API security risks for the year 2019. (API1:2019 - Broken object-level authorization).
Context
In most of the API implementations, where we have to get some data which is specific to some object, Reference of the internal object implementation is exposed, for example: "id", "pid", "uid" and so on. Although in most cases this reference is visible as part of the HTTP parameter itself, but these could also be part of headers and cookies.
The problem here is that it reveals the real identifier and format/pattern used of the element in the storage backend side. The most common example of it (although is not limited to this one) is a record identifier in a storage system (database or filesystem).
Once the pattern is identified, the malicious user can easily guess/autogenerate the reference ID's and modify the requests to supply the reference ID's that do not belong to them.
If the API implementation does not have proper measures in place, then these malicious requests can cause revelations, modifications, or even deletion of records in the backend.
Just because the direct object reference checks were not in place or misconfigured. That is the reason this attack is also known as Insecure Direct Object Reference or IDOR in short.
Use Case
Let us assume that we have an API implementation that gets the financial information of a user. And to get the details the API expects the user id as parameter.
- API call parameters use the ID of the resource accessed through the API /API/user1/financial_info.
- By proxying and intercepting the URL's attacker can guess the format of the parameter (user1).
- Attackers replace the IDs of their resources with a different one which they guessed through /API/user2/financial_info.
- As there is no measure to check the check permissions, the API returns data for user2. Which, it should not have.
- This could cause further issues if IDs can be enumerated and calls are automated to scrape though the data available with the UI /api/{AUTOMATED_USER_ID_HERE}/financial_info.
How to Prevent it
If we have to summarize then we can say, IDOR is a combination of two issues
- Enumerable identifiers, which could easily be guessed.
- Insufficient checking of access rights on the requests.
So, the solution to prevent IDOR completely has to address both the issues, Below are some of the recommendations.
- API should verify that the current user has permission to access the object, every time access is attempted. For example, if a user has access to record 3 and no others, any attempt to access record 4 should be rejected.
- The API should validate both read and write requests, it should not be the case that a user can write to a record, even without having the read permission to it, such scenarios may allow the further compromise of the system. For example, if a PUT request made to /data/1 contains a sensitive value, it is important to check that the same PUT request cannot be made to /data/2, even if it is known that GET /data/2 is rejected.
- Implement authorization checks with user policies and hierarchy.
- Do not rely on IDs that the client sends. Use IDs stored in the session object instead.
- Check authorization for each client request to access the database.
- Use random IDs that cannot be guessed (UUIDs).
Sample Application
As we saw earlier that our solution has to address both the Access and predictable ID issue. Let us try to build an application to demonstrates the same. Please be advised that this application is meant for demonstration purposes only.
So, our hypothetical application has a requirement that the users shall be able to get his data (e.g. financial details). Now, the API design could take user-id as input and serve returns the financial details belonging to the user. The common implementation approach for the API will be to have the user id as path parameters or as part of headers. Let's say we choose the path parameter. Then our API will look something like this. And it works as expected.
xxxxxxxxxx
http://localhost:9999/getdata-IDOR-vulnerable/user_2
But by analyzing the request we can see that the input is user_1, what if we supply user_2?
xxxxxxxxxx
http://localhost:9999/getdata-IDOR-vulnerable/user_2
Voila! it works again. Thus, it is clear that user_x is a reference to internal ID. Now, what if we somehow automate this parameter using some tool, or even simple programming? We can get the data of all the users available in the system.
xxxxxxxxxx
http://localhost:9999/getdata-IDOR-vulnerable/user_2
http://localhost:9999/getdata-IDOR-vulnerable/user_3
http://localhost:9999/getdata-IDOR-vulnerable/user_4
.
.
.
http://localhost:9999/getdata-IDOR-vulnerable/user_N
We leaked all the data just because someone was able to guess the reference ID.
So, what if we use something difficult or random to guess? We solve one part of our problem.
One of the possible ways could be that we use these complex ID's (something like UUID) in our backend system, or we can wrap our data fetch logic around some utility class or filter which does this job for each request. In a very basic example, we could write something like below.
xxxxxxxxxx
package org.sk.owasp.api.security.idor.util;
import java.util.Base64;
import org.springframework.stereotype.Service;
/**
* <strong>FOR DEMONSTRATION ONLY, NOT TO BE USED IN PRODUCTION.</strong><br>
* As this code is for demonstration purpose only. {@link Base64} is used to encypt-decrypt the ID's. This algorithm is not considered safe and can be
* decoded easily.
* In real world scenario you shall be using more sophisticated algorithms or something like UUID
* Access check in intentionaly left from the code.
* @author Satish Sharma
*
*/
public class IdorUtility {
// this could be externalized to configuration.
private String saltString="S3cUr#d1Ds1L$";
public String computeObfuscatedId(String actualIdentifier) {
String saltedString = actualIdentifier + saltString;
return new String(Base64.getEncoder().encode(saltedString.getBytes()));
}
public String resolveObfuscatedId(String obfuscatedIdentifier) {
var decodedBit = Base64.getDecoder().decode(obfuscatedIdentifier);
String decodedSaltedString = new String(decodedBit);
return decodedSaltedString.replace(saltString, "");
}
}
Once we wrap our service call around the encode-decode utility class. As we have encoded all the identifiers with Base64, The API request will change something like below. Notice the change in the path parameter in the URL.
xxxxxxxxxx
http://localhost:9999/getdata-IDOR-secured/dXNlcl8zUzNjVXIjZDFEczFMJA%3D%3D
As we can see now the IDs cannot be guessed easily. Combined the above code with authorization mechanism (left intentionally from this code). We address both the issues, and hence prevent the IDOR.
You can find the sample spring-boot application at this GitHub location. In the next article, we shall be addressing API2:2019 Broken User Authentication.
Thank you for reading. If any questions or comments please share them, I would be happy to hear.
Opinions expressed by DZone contributors are their own.
Comments