APIs are everywhere. In most IT systems (mobile applications, web platforms, cloud infrastructures, etc.) and in all sectors of activity, these programming interfaces facilitate the exchange of data and their availability to a wide audience, whether customers, partners, or employees. APIs are also the driving force behind the development and growth of connected objects, as they form the basis of the communication channels of IoT systems.
In 2019, a study published by Akamai estimated that 83% of all web traffic was generated by APIs. This trend continues as APIs have become the easiest way to expose features and data in an information system.
However, while the spread of APIs has led to increasingly complex applications that improve business use and performance, it has also led to a sharp increase in cyber risk. Indeed, their exposure and their “critical” nature – because they handle sensitive data – make them prime targets for cyber attacks. The security of APIs must therefore be at the core of a cybersecurity strategy.
Why is API security important?
Attacks targeting APIs are one of the most serious security threats facing businesses, as they provide direct access to sensitive data and functionalities. And attackers have become aware of the popularity of APIs and the existence of critical vulnerabilities in these interfaces. The problem is that web applications remain the primary target of attacks (90% according to Verizon*) and APIs now represent 90% of the attack surface of web applications. Thus, APIs have become one of the main attack vectors, with devastating financial consequences for the companies that bear the costs.
Furthermore, because APIs use web technologies, an API developer will face common vulnerabilities within this ecosystem. Most of the risks associated with web applications are applicable to APIs, but due to their nature, they extend the attack surface and face unique risk factors. While web applications are designed to provide access to a specific user interface and expose specific functionality from the back-end, APIs provide a much more flexible channel to the back-end, in terms of the amount of information that can be retrieved from the servers. Indeed, APIs facilitate the transfer of large amounts of data much more easily than web applications can.
An API can be called multiple times to generate a DoS attack. In addition, firewalls, antivirus, and other technical security solutions cannot block attacks that exploit specific vulnerabilities in APIs. They cannot detect injection attacks, or logical attacks on parameters or workflows. Similarly, vulnerability scanners used for automated security audits cannot identify most of these vulnerabilities – especially the “VERY” common logical vulnerabilities in APIs, unlike penetration testing, which follows a proven methodology that includes a “VERY” valuable vulnerability exploitation phase, enabling not only the identification of other vulnerabilities, but also the assessment of their impacts and potential side effects.
Security should therefore be an essential part of any organisation’s API development strategy. The objective of this article, which is not intended to be exhaustive, is to outline the best practices for securing your APIs, and to explain the common vulnerabilities that malicious attackers exploit to compromise them. We will illustrate some of the risks through real cases encountered during our API penetration tests.
But before we get to the heart of the matter, a few details on the differences and specificities of the main types of APIs used today.
REST API, SOAP & GraphQL: what are the differences and specificities of these main types of APIs?
APIs are interfaces that exchange commands and data, which requires clear protocols and architectures. There are 3 main types of APIs: REST, SOAP and GraphQL, each with specific characteristics.
Characteristics of REST APIs
The REST (representational state transfer) architecture is the most common approach to building APIs. REST is based on a client-server relationship that separates the front- and back-end parts of the API and offers great flexibility in development and implementation. REST is stateless, which means that the API does not store any state or data between requests.
REST APIs are based on the HTTP protocol and support TLS encryption. The TLS protocol (to be discussed later in this article) keeps a connection and ensures that data exchanged between two systems (server and server, or server and client) is encrypted and not readable in the event of a Man In The Middle attack.
REST APIs also use the JSON (JavaScript Object Notation) file format, which facilitates data transfer through web browsers.
API and SOAP protocol
SOAP is a messaging standard defined by the World Wide Web Consortium (W3C) and used to create APIs written in XML. SOAP supports a wide range of communication protocols, such as HTTP, SMTP and TCP.
The SOAP approach, through the Web Services Description Language (WSDL) IDL, determines how a message is processed, the functionalities and modules included, the communication protocol(s) supported.
Unlike REST, SOAP is a highly structured, tightly controlled and clearly defined standard. For example, SOAP messages can contain up to four components, including an envelope, a header, a body and a default (for error handling). In addition, SOAP APIs use built-in protocols known as WS Security (Web Services Security). These protocols define a set of rules that allow the addition of privacy and authentication mechanisms (XML encryption and signatures, SAML tokens, etc.).
GraphQL APIs
GraphQL is a query language and execution environment developed as an alternative to the REST architecture. Indeed, GraphQL APIs have their own architecture based on a query schema system.
What are the common vulnerabilities of APIs and how to protect yourself?
Lack of rate limiting, DoS and brute force attacks on APIs
Principle and functioning of DoS attacks
A Denial of Service (DoS) is an attack carried out with the aim of making services unavailable. Indeed, a DoS attack works by exhausting a limited resource that an API needs to respond to legitimate requests. By flooding an API with false requests, its resources are blocked from responding to those requests and not to others.
The objective of DoS attacks is not to alter, delete or steal data. The aim is simply to damage the operation of a web service or the reputation of a company offering such services.
Thus, companies whose main activity is based on a flow of information over the web (through an API or any other web service) are at risk and may be paralysed at any time. Furthermore, malicious actors use DoS attacks as a weapon to blackmail companies. The message is clear: a ransom is requested in exchange for “non-paralysis” of their business. It is obvious that slowing down or even blocking their services for a few minutes could lead to significant financial losses and alter users’ trust. It is therefore necessary to find solutions to protect against this, including request verification, traffic monitoring, the implementation of rules and rate limiting, etc. Similarly, during an API or web application pentest, you can include DoS tests to assess the resistance of your services to this type of attack.
Brute force attacks on APIs
In a brute force attack, an attacker uses tools to send a continuous stream of requests to an application or API – to test all possible combinations of a parameter, through a process of trial and error in the hope of guessing right. The objectives can be multiple: brute force of an authentication form to steal an account, brute force of a login to retrieve sensitive data, brute force of a secret, etc.
This is a “trivial” attack method, easy to perform, but still very effective and widely used by attackers.
Implement rate limiting mechanisms to counter DoS and brute force attacks
Securing APIs against DoS or brute force attacks requires the implementation of rate limiting mechanisms. These mechanisms protect APIs and other services from excessive and abusive use, in order to ensure their availability.
The principle of rate limiting is very simple. It involves anticipating the fact that one or more clients – systems – may use more than their “fair” share of a resource, by sending requests. And by limiting the number of requests that a given user is allowed to send in a defined period of time, one can reduce the risk of DoS or brute force attacks.
For example, after a user has been authenticated, your API or application may apply quotas that restrict what they are allowed to do, including the limit of requests they can send. For example, you can limit each user to a certain number of API requests per hour, to prevent them from flooding the system with too many requests.
Similarly, before authenticating a user, you can apply limits to restrict the number of requests overall, or from a particular IP address or time slot. So, if rate limiting is implemented, your API will keep track of the number of requests and reject those above the allowed limit. Furthermore, you can apply rules to close connections completely when the limit is exceeded or to slow down the processing of requests. This process is known as “throttling”.
In short, Rate Limiting prevents resource depletion by managing rules and quotas. There are different techniques for applying rate limiting, each with its own specificities: Token bucket, Leaky bucket, Fixed window and Sliding window.
To conclude this section, a piece of advice from pentesters: don’t reinvent the wheel! Vulnerabilities often occur due to custom developments of critical features or mechanisms. There are many known, proven and robust frameworks for implementing efficient and secure rate limiting mechanisms. You should therefore favour these solutions to avoid opening other loopholes.
Lack of user input validation and injection attacks
Code injections
Code injection is one of the most common types of injection attacks. If attackers know the programming language used by an application or API, they can inject code through text input fields to force the web server to execute the desired instructions.
SQL injections (SQLi)
Injections represent a significant part of the vulnerabilities encountered in applications and APIs. The best known and most dangerous is SQL injection.
In an SQL injection attack, an attacker injects data to manipulate SQL commands, thereby interacting with the database through unintended queries. These flaws can lead to theft, deletion or manipulation of stored data. Worse still: if the rights are too permissive, this can even lead to a compromise of the server.
Let’s look at this in more detail with a concrete example:
One can imagine an API endpoint that returns the information of a country based on its “Country_Code”.
URL: http://localhost:8042/?CC=FR
[
{
"name": "France",
"code": "FR"
}
]
To perform the desired action, our query interacts with a database. Below is the SQL query that the server must run on the database.
SELECT name_fr as name , alpha2 as code FROM Country WHERE alpha2= "FR"
A vulnerable implementation of this action is to directly concatenate the value of the “CC” parameter into the SQL query.
Example of a vulnerable PHP code:
<?php
try{
$db = new PDO('sqlite:database/base.sqlite3');
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // ERRMODE_WARNING | ERRMODE_EXCEPTION | ERRMODE_SILENT
} catch(Exception $e) {
echo "Unable to access the SQLite database:".$e->getMessage();
die();
}
$recipesStatement = $db->prepare('SELECT name_fr as name ,alpha2 as code FROM Country where alpha2 = "'.$_GET['CC'].'"');
$recipesStatement->execute();
$recipes = $recipesStatement->fetchAll();
header('Content-Type: application/json; charset=utf-8');
echo json_encode($recipes, JSON_PRETTY_PRINT);
As we can see, the CC parameter that is controlled by the user is directly concatenated to the query.
Now, let’s assume that on this same database, there is a ‘users’ table that contains the email addresses and passwords of the users registered on the application. Let’s look at what happens if an attacker makes the following query:
URL: http://localhost:8042/?CC=FR%22+UNION+SELECT+email,password+FROM+user--
In addition to the country name, this query retrieves all users and their password hashes.
This is an SQL injection exploit. A vulnerability that allows an attacker to “pervert” the SQL query generated by the application. With this behaviour, it is possible for the attacker to read or even alter the data in the database. This is obviously a critical vulnerability that would be reported if it were an API or web application penetration test.
Validate user input to prevent injection attacks
Validating user input is the best defence against SQL and code injections. In theory, it should be assumed that data received by an application or API cannot be considered “always” safe. In practice, it is a matter of implementing mechanisms to check that user input matches the expected parameters to avoid such vulnerabilities.
The most effective method of protecting against SQL injections is the use of prepared statements, which separate the SQL commands from the data sent by a user.
To illustrate this method, let’s go back to our example mentioned above, this time with the remediation tip.
The fix is to use prepared queries. On the PHP documentation we can see the following information:
- The query only needs to be analysed (or prepared) once but can be executed several times with the same or different parameters. When the query is prepared, the database will parse, compile and optimise its plan to execute the query. For complex queries, this process can be quite time consuming, which can slow down your applications if you have to repeat the same query several times with different parameters. By using prepared statements, you avoid repeating the analysis/compilation/optimisation cycle. In short, prepared queries use fewer resources and run faster.
- Parameters for preparing queries do not need to be enclosed in quotes; the driver handles this for you. If your application uses prepared statements exclusively, you can be sure that no SQL injection is possible (however, if you build other parts of the statement based on user input, you are still taking a risk).
Thus, the following code is correctly protected against SQL injections:
<?php
try{
$db = new PDO('sqlite:database/base.sqlite3');
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // ERRMODE_WARNING | ERRMODE_EXCEPTION | ERRMODE_SILENT
} catch(Exception $e) {
echo "Unable to access the SQLite database:".$e->getMessage();
die();
}
$recipesStatement = $db->prepare('SELECT name_fr as name ,alpha2 as code FROM Country where alpha2 = :CC');
$recipesStatement->execute([
'CC' => $_GET['CC']
]);
$recipesStatement->execute();
$recipes = $recipesStatement->fetchAll();
header('Content-Type: application/json; charset=utf-8');
echo json_encode($recipes, JSON_PRETTY_PRINT);
As we can see, the difference with the vulnerable code is that the parameters coming from a user are no longer concatenated with the query, but directly provided at query execution. This also shows that a prepared statement can still be vulnerable to SQL injection if the data is concatenated. Because in the vulnerable example it was already a prepared statement that was used.
Lack of data encryption and Man In The Middle attacks
Man In The Middle attacks
A Man In the Middle attack is a type of attack in which a malicious individual inserts himself into a communication or data transfer between a client and a server, a server and a server or a client and a client. Its objective can be multiple: it can be simply to intercept sensitive data (passwords, banking information, personal data, sensitive documents, etc.) or to manipulate the communication in order to introduce malware for example.
This type of attack is possible if and only if the communications are not encrypted. It is therefore quite easy to protect against it.
Encrypting data with TLS to counter Man In The Middle attacks
Encryption is one of the most basic aspects of ensuring the security of an API or application. Indeed, TLS (successor to SSL) is an encryption protocol that ensures secure communications over a computer network. When secured by TLS, connections between a client and a server have one or more of the following properties:
- The connection is private (i.e. secure) because the data transmitted is encrypted.
- The encryption keys are uniquely generated for each connection and are based on a shared secret negotiated at the beginning of the session.
- The connection guarantees integrity because each transmitted message includes a signature verification of the integrity of the message, thus avoiding any undetected loss or alteration of the data during transmission.
The use of this encryption protocol therefore reduces or even eliminates the risks of Man In The Middle attacks. Furthermore, to reinforce security, we recommend implementing the HSTS (http Strict Transport Security) header on your servers in order to force a browser to use HTTPS secure connections. Without this setting, you run the risk of users accessing your domain without the HTTPS protocol, which can lead to a breach in communications.
Security of authentication and authorisation in APIs: JWT tokens, OAuth 2.0 and OpenID
JWT Token Implementation
JSON Web Tokens (JWT) are generated by a server when authenticating a user, before transmission to the client. They will be returned with each HTTP request, allowing the server to identify the user. To do this, the information contained in the token is signed using a private key on the server. When it receives the token again, the server simply compares the signature sent by the client with the one it generated with its own private key and compares the results. If the signatures are identical, the token is valid.
A JWT token is therefore used to authenticate users and thus assign them rights. A bad implementation can therefore be a source of vulnerabilities. Indeed, a user could for example increase his rights on a platform (raise his privileges and become admin for example) or access the data of another user.
A common vulnerability: server-side misconfiguration of tokens. To illustrate this vulnerability, a quick aside on the components of a JWT Token. In most cases, a JWT token generally contains:
- A header, the algorithm used for the signature, in JSON encoded in Base64 (usually HS256).
- A payload, the token information. This can be the user’s name, role or email.
- A signature, which corresponds to the concatenation of the “Header” and “Payload” parts encrypted with the private key (held by the server).
The JWT standard allows the non-use of an algorithm to sign the token. This is of course a very bad practice, because in the case mentioned before, a user could change his rights on the fly (from “role”: “user” to “role”: “user2” or “role”: “admin”), without any control by the server. In the same way, if the configuration carried out at the server level accepts different algorithms and the “none” variable, which consists of not having cryptographic functions, it will be possible for an attacker to bypass the integrity verification function to access the data of other users or even of the admin.
Nothing could be simpler to avoid this type of risk: choose a reliable algorithm (HMAC SHA256) and configure the server to always check, and therefore reject the “none” variable.
Furthermore, attackers can try to guess the private key held by the server, using brute force. A word of advice! Avoid defining a string of characters that is too obvious because a bad password can be cracked without too much effort, with the right tools and very little hacking skills.
Another common case is the exposure of bearer tokens in URLs by some API features. This configuration exposes the tokens (and thus the security of the related accounts) to risks, because browsers, servers and other intermediary elements do not necessarily ensure the security of URLs.
The remediation here is to make the API functionality pass Bearer tokens through HTTP headers. This is also a recommendation for the use of Bearer in OAuth2 (which we will discuss in detail in a future article). And if it is technically difficult or impossible to move the tokens in the headers, some measures can still reduce the risk of compromise. In particular, it is important to ensure that:
- The clients making the requests include the “Cache-Control: no-store” header
- The server also returns a “Cache-Control: no-store” header in its responses
- The lifespan of the token is low
For more information on JWT tokens, you can read our dedicated article on this topic: JWT tokens and security – working principles and use cases.
OAuth 2.0 & OpenID
OAuth 2.0 & OpenID are essential standards for securing authentication and authorisation in APIs. OAuth 2.0 allows an application of any type to use a third-party API on behalf of a user. OpenID is a protocol for verifying a user’s identity with an authorisation server to obtain information about the user.
These 2 protocols are considered today as key elements of API security. This is why we will dedicate a separate article to them, which will review the principles, use cases and configuration of OAuth 2.0 & OpenID.
Subscribe to our Security Digest (one email per month on security-oriented content) to receive it as soon as it is published:
Rights issues and attacks on logical API processes: Mass Assignment, Lack of access control and IDOR
In the case of a REST API, for example, the norm is to manipulate an entity through predefined methods. For example, let’s imagine a file management platform, with a file entity that contains the following attributes:
- Id
- Name
It is therefore easy to deduce the following potential REST calls:
Method | Endpoints | Description |
GET | /file | Retrieves all files from the platform |
GET | /file/<id> | Retrieves the file with the id |
POST | /file | Create a file |
PUT/PATCH | /file/id | Replaces/Modifies the file with the id |
DELETE | /file/id | Delete the file |
This is obviously an assumption about how the methods work. File retrieval (GET /file) may, for example, rely on the user’s ID and return only their files.
It is therefore easy for an attacker to adapt REST requests to try to manipulate resources. It is thus important to ensure that each access correctly checks the user’s rights and reacts accordingly.
Mass Assignment
In some cases, the application accepts that the user can modify a resource, but only a restricted set of attributes. For example, an application allows a user to modify their profile. We will send the platform a JSON of this form:
#POST request body
{
"firstname":"Billy",
"lastname":"Doe"
}
The response from the platform is as follows:
#Response body
{
"firstname":"Billy",
"lastname":"Doe",
"role":"User"
}
If the API is not well configured, it may be possible for Billy to send the following request:
#POST request body
{
"firstname":"Billy",
"lastname":"Doe",
"role":"super_super_Admin"
}
And the application will return:
#Response body
{
"firstname":"Billy",
"lastname":"Doe",
"role":"super_super_Admin"
}
This shows that the application allows anyone to change its role, and therefore allows privilege escalation.
Lack of access control and IDOR
Insecure direct object references (IDORs) are common vulnerabilities that allow attackers to bypass permissions and access resources directly by changing the value of a parameter used to point directly to an object. These resources can be database entries belonging to other users, files in the system, etc.
It is common on an API to access a resource through id. If the rights are not properly checked by the application, an attacker can easily modify the request to access objects that do not belong to him. For example Billy sees that the request according to him allows him to retrieve his file:
#GET https://ultra-random-website.com/file/42
And the API returns the following response:
#Response body
{
"id":42,
"url": "//resource_image_cat_78b369a7-960e-4e02-bdc7-4546c741ea65.jpg"
}
So he decides to replay the following query and see the result:
#GET https://ultra-random-website.com/file/41
Result:
#Response body
{
"id":41,
"url": "//resource_very_sensitive_info_49f18c2b-1f5c-452a-b222-9d1eea743123.jpg"
}
He is able to retrieve a URL that does not belong to it and therefore potentially gain access to files that do not belong to it. This is because the application takes the input provided by the user and uses it to retrieve an object without performing sufficient rights checks.
It is therefore necessary to ensure for each request that the user has the correct rights to operate it.
Also, in some cases, applications use UUIDs (e.g. 736fb654-a3ac-446c-8cdf-3afd85c47e06) and rely on security through obscurity. Security by obscurity can be a plus, but it is still recommended to make sure that the user has the appropriate rights even with ids that cannot be guessed. Indeed, it happens that data leaks allow uuid to be discovered.
Check user rights to prevent privilege escalation
Access control issues, generally associated with a lack of access control, are surely the most widespread vulnerabilities in APIs.
To prevent this, the rights of a user making a request must be systematically checked by the server. The request should only be accepted if the user has the appropriate rights. In general, the web server should always check the user’s rights to use a feature and the parameters entered.
Vulnerabilities of GraphQL APIs
GraphQL APIs share some of the common vulnerabilities of REST APIs mentioned in the core of this article. However, because their architecture is different, they face specific risks: introspection, access control issues, etc.
We will come back to the inherent vulnerabilities of GraphQL APIs in a dedicated article, with, as always, concrete examples and best practices to protect against attacks.
Perform an API penetration test to assess risks and strengthen security
APIs are very attractive targets for attackers because of their exposure, their core nature in the Information System – given the sensitive data that flows through them – and the many vulnerabilities. The security of your APIs is therefore essential. This requires the implementation of best practices in terms of development, configuration, integration, and above all offensive security audits (i.e., penetration tests) to evaluate the resistance of your APIs to real attacks.
Indeed, a penetration test consists of testing any type of system with a dual objective: to identify vulnerabilities that an attacker could exploit and to recommend security fixes. During an API penetration test, it is particularly important to look for vulnerabilities both on the server side and on the application layer.
Examples of server-side vulnerabilities:
- Open and insecure services
- Unupdated or outdated software
- Bypassable security elements
- Configuration errors
Examples of API vulnerabilities (OWASP Top 10):
- Broken Object Level Authorization
- Broken User Authentication
- Excessive Data Exposure
- Lack of Resources & Rate Limiting
- Broken Function Level Authorization
- Mass Assignment
- Security Misconfiguration
- Injection
- Improper Assets Management
- Insufficient Logging & Monitoring
Moreover, a penetration test, unlike automated tools (vulnerability scanners), includes a manual phase of exploiting vulnerabilities, which are generally used as a “pivot” to discover other vulnerabilities.
Regardless of the scope of the tests, a full report is produced following any penetration test. It includes the methodology followed, the vulnerabilities identified, the level of criticality, the possible exploitation as well as recommendations for correction. The penetration test may be completed by a validation phase of the fixes to verify their correct implementation and the absence of side effects.