Application programming interfaces, or APIs, connect the software and services we use by enabling smooth data exchange. Often, they exchange highly sensitive information: personal data, user credentials, financial details, etc. That’s why APIs are popular targets for hacking attacks — 91% of companies experienced an API security incident during 2020 according to the State of API Security report [PDF] by Salt Security.
In this article, we overview the most widespread API vulnerabilities from the OWASP API Security project, show how exactly they can be exploited, and offer ways to protect your APIs from such security issues during development.
This article will be helpful for security testers and developers that want to improve the security of their APIs.
When working on application security, protecting APIs is one of the key tasks, since APIs are often gateways for attackers. Gartner predicts [subscription required] that by 2022, API abuses will become the most frequent vector of hacker attacks and will lead to many data breaches.
Prioritize your efforts at securing your APIs by focusing on the top 10 security risks for APIs according to the API Security Top 10 (2019) list provided by the OWASP API Security Project.
Understanding how attackers can exploit weaknesses in your code is one of the key steps in securing against risks during API development. In this article, we’ll show you examples of how exactly attackers can exploit the first six vulnerabilities from the list. We won’t focus on the last four API security challenges, because they’re related to improper application of security mechanisms.
To show you how a malicious actor can exploit these vulnerabilities, we’ve created an unprotected API. We’ll show which parts of its code open the door to hackers and discuss how you can fix them. Let’s start with deploying an unprotected API.
For this article, we’ll create an API for a simple task management system. The system has users with different access levels and allows them to carry out simple tasks. Also, this system allows for user management activities: self-service registration, creation, editing, and deletion of user accounts.
The API will have the following endpoints:
You can use any Linux distribution as the environment for this API. To deploy the API, follow these steps:
- Install Docker
- Install Docker Compose
- Configure the email address in local.cfg (you’ll need this address to send password reset emails)
- Go to the API folder and execute the docker-compose up --build command
You can also download this sample API from our GitHub repository. The credentials for the API administrator account are:
- username: admin
- password: admin
To read the API documentation, upload the ./swagger/swagger.yaml file from the API to Swagger Editor. When the API is deployed, we can start exploiting vulnerabilities and fixing them.
Some APIs expose object identifiers, which are essential for access control mechanisms. These mechanisms validate that a user can access only those resources for which they have access rights. To exploit APIs with broken object level authorization, attackers change the authentication data of the requested resource in the API call and obtain access to protected data.
In our sample API, only users themselves and administrators can review users’ account details. Also, we added a security flaw during API development and made sure that the GET /user endpoint contains an object level authorization vulnerability. To detect it, we need to:
- Register two users
- Log in to the system as user1 via POST /login endpoint
- Acquire an authentication token
We log in to the system with this request:
The API responds to our request with the following data:
Here’s our authentication token:
If we can acquire the token, we can use it to request user2 data via GET /user endpoint:
Our vulnerable API responds with user2 data:
If our API were protected against the object level authorization exploit, it would respond to the GET /user endpoint request with the following message:
How can you fix it?
To avoid broken object level authorization, make sure your authorization mechanisms check user hierarchy and access rights each time before providing users with access. The API needs to check user rights even after users log in to an application. Also, you can randomize user profile characteristics such as user IDs to make them harder to guess.
Issues with user authentication can allow attackers to impersonate users, access their personal data, and abuse access privileges. Usually, such vulnerabilities are hidden in password reset mechanisms. Let’s see how we can exploit broken user authentication in our API.
We start by executing this password reset request:
When the script enters the correct reset code, we’ll get a response with the new password:
By exploiting this object level authorization vulnerability, we can obtain the login of the user that has this email, log in to their account, and change the email.
In our sample API, there’s a security threat that allows us to reset a user’s password several times using the same reset code. We can pass this request with code 1111 and change the user’s password any time we want:
How can you fix it?
Wherever possible, it’s best to implement multi-factor authentication to confirm the identity of the person trying to log in to an account. Also, treat credential recovery as an additional login endpoint: remember that credential recovery systems can also be brute forced. That’s why you need to implement a limitation on login attempts, CAPTCHA mechanisms, and other security measures.
This API security vulnerability may appear when developers implement generic mechanisms for communication between an API and a client. In such a case, an API may send a client more data than it needs, and the client has to filter the data and hide irrelevant information from the user. Attackers can sniff this traffic and extract sensitive information from it: authentication tokens, account numbers, email addresses, etc.
To demonstrate this vulnerability in our API, we’ll request a task’s ID and full information about the user from the GET /task endpoint. This endpoint is supposed to return only the task ID, but let’s see what happens.
Here’s our request:
And here’s how the GET /task endpoint responds:
If an attacker intercepts this response, they will obtain all the information the API has about the user, even if it’s unavailable in the API’s client.
How can you fix it?
Relying on an application to filter data from an API is a bad cybersecurity practice. Instead, you should analyze which sensitive data your API has access to and review all possible responses from your API. Configure responses in such a way that they contain only the information the user requests and nothing else.
APIs can use CPU, RAM, and disk resources to process requests. Developers usually choose the resources allocated for an API according to the application’s business logic. If an attacker manages to bypass business logic limitations to create a situation where the API has to process more requests than it was designed to, the application will run out of resources and start malfunctioning or become unavailable.
In our API, GET /tasks contains this vulnerability. This endpoint supports paging — storing data from RAM on the hard drive. An attacker can abuse this capability to overload the API.
Let’s suppose that the application displays 10 tasks on one page. A request for displaying tasks will look like this:
An attacker can send their own request with an enlarged size parameter to overload the API:
If there are too many tasks in the database assigned to the user that requests tasks, the API will be overloaded, leading to the denial of service.
How can you fix it?
You can prevent a lack of resources and denial of service by limiting the resources that can be allocated for an API and the number of calls from the client to the API in a defined timeframe. This will create rate limiting, and the API will notify the application when it reaches the resource limit.
Another useful practice is to define the maximum size of data in request parameters that an API can process without slowing down.
Misconfigured authorization mechanisms allow attackers to gain unauthorized access to sensitive resources and steal, edit, or create new user accounts. To detect this vulnerability, attackers send requests to access objects they shouldn’t be able to access.
We added the GET /admin/users endpoint to our API to demonstrate this vulnerability. This endpoint returns data on all users registered in the application without checking who requested the data (a user or an admin). Here’s an example of such a request:
The GET /admin/users endpoint responds with the following code:
How can you fix it?
Enforce an authorization mechanism that denies any access to the API and the application by default. It should provide users with access only if they provide relevant credentials, belong to a certain group or role, or pass multi-factor authentication. After that, users should be allowed to access only the data they requested; when they request new data, they have to be authorized once again.
Some developers design their APIs to assign object properties automatically when binding input from the application into code and internal objects. Many frameworks provide mass assignment functions to help speed up development.
This approach is convenient for developers, but it also allows a user to change object properties that they shouldn’t access. Also, attackers can try to guess object properties or substitute them with new ones at their request. If an API is vulnerable to such requests, attackers can gain information about sensitive objects, read the documentation, or modify data objects.
In our API, the user object has this vulnerability. It has a user_type parameter with two possible values: user and administrator. When people register in our application by themselves, their accounts are assigned the user value by default. However, our API allows users to change this value via the PUT /user request. In this way, an attacker can gain administrator privileges.
To exploit this vulnerability, we’ll have to register a user with this request:
After that, we’ll get a response with user account details:
Then, we need to change the user_type field to admin:
The API will respond with updated user data:
In this way, user4 will gain administrator access rights.
How can you fix it?
If you use a framework that automatically assigns code variables or object properties, define those parameters manually. To avoid unwanted changes in data that can be retrieved from the API, add the readOnly property for all such data.
Another way to detect and stop mass assignment attacks is to implement user and entity behavior analytics (UEBA) in your application. A UEBA system will figure out the normal behavior for an API and alert you of suspicious actions like changing objects that the API usually doesn’t change.
Mitigating security issues from the OWASP API Security project is critical for ensuring an application’s protection. To prioritize testing procedures and save some time, you can focus on finding and fixing the most widespread vulnerabilities that we discussed in this article.