Types of web API, SPA, and JWT vulnerabilities in 2022

Lecture



APIs come in many shapes and sizes, and attack methods against APIs will vary greatly depending on those shapes and sizes. It would be impossible to describe every type of attack in a single blog post, but we'll cover several of them at once!

The nature of a Web API

Like web applications, APIs can have different levels of visibility. Some may be exposed on the Internet, while others are only intended for internal use. One of the most basic ways to hack an API is simply to gain access to an API that should not be accessible to you. This can be achieved through various methods, including:

  • Forced browsing: if you're lucky, an API intended for internal use may be accidentally exposed to the Internet, either due to a misconfiguration or simply because it was assumed no one would find it. The location of the API can be discovered in many ways, including analyzing JavaScript files, open-source intelligence analysis, observing hostnames (for example, api.internal.example.com), and Google dorking.
  • Pivoting: discovering an exploit, such as SSRF, on an external host may allow you to pivot to an internal API.

Primary defense

There are many best practices that can help prevent unintended API exposure, including implementing strict deployment practices, enforcing the principle of least privilege through IAM, and network segmentation.

Misconfigured caching

For APIs that require authentication, the data returned is often dynamic and tied to each API key. For example, accessing /api/v1/user as Mystic should return Mystic's details, and accessing the same endpoint as Intella should return Intella's details.

A common misconfiguration occurs when an API doesn't use the standard Authorization header but instead uses a custom header, such as Some-API-Key. Caching servers may fail to recognize this as an authenticated request and may cache it.

If this is the case and the Cache-Control or Pragma headers are missing, simply accessing /api/v1/user may expose another user's information.

Primary defense

The fix for this is to implement Cache-Control or Pragma headers and to use the standard Authorization header.

Exposed tokens or misconfigured tokens

We shouldn't discount the most basic method of authentication. Discovering an API key by any means can grant you access to the API. Worse still, APIs intended for internal use often don't need to implement complex authentication flows, and thus may implement a static token as their authentication. Secret tokens can be discovered in code repositories, client-side JavaScript, intercepted traffic, and so on.

Primary defense

Introducing code scanning into the DevOps pipeline often results in API keys being discovered before they're deployed somewhere they shouldn't be. Some code repository providers (including GitHub ) also have the ability to detect API keys before they're pushed.

JWT weaknesses

If your API token is three base64 blobs separated by two dots (.), it's probably a JSON Web Token (JWT) . Like many other things, these tokens are theoretically secure, but there are many ways to botch the implementation, leading to security issues. Before we dive into JWT attacks, we'll cover a very brief guide to JWT.

Here's an example JWT token, colored for your aesthetic pleasure:

Types of web API, SPA, and JWT vulnerabilities in 2022

There are three base64-encoded strings separated by dots; the first section (colored red) is the header. The second (colored purple) is the payload, and the third (colored blue) is the signature.

If we decode the first section, also known as the header, we'll see the following:

{
"alg": "HS256",
"typ": "JWT"
}

This describes the algorithm we're using (HS256) and the token type (JWT). If you haven't worked with JWTs before, you might be thinking: «Why do we need an algorithm?» . We'll get there soon!

If we decode the second section, also known as the payload, we'll see the following:

{
"sub": "1234567890",
"name": "Intella Icu",
"iat": 1616161616
}

This section can contain anything, but at minimum it should contain some kind of user identifier and a timestamp (iat).

The third section (also known as the signature) signs the first two sections with a secret key. In this case it's signed using the HS256 algorithm, which can be determined by looking at the «alg» value in the header. In principle, the secret key should only be known to the application owner. When the application receives a JWT token, it can verify its legitimacy by decrypting the signature and comparing it against the data in the header and payload. If the data matches, the data is verified; otherwise it's invalid.

So why would anyone use JWT? It's because it eliminates the need for server-side session management. Normally, when a user logs in, the application assigns them a secret token and stores that same token in the database. Every time the user makes a request with their token, the application has to check whether the token is in the database. If it is, the user is allowed to proceed; otherwise, they aren't. When we use JWT, we introduce a method of trusting the data sent from the client, instead of fully trusting the information stored in the database. If the application receives a JWT token that can be verified using the secret key, the application has no reason not to trust it.

As we mentioned earlier, JWT tokens are theoretically completely secure. The problem is that they're often implemented insecurely. Here are some examples:

  • The «None» algorithm: some JWT implementations allow you to specify «None» as the algorithm. If the algorithm is «None», the application won't verify authenticity using the signature, so you can simply update the payload to whatever you want. The most obvious use of this would be to update the user ID to another user's, in order to gain control of their account.
  • Brute-forcing passwords: it's possible to brute-force the JWT token's secret key. The feasibility of this attack depends on the strength of the key.
  • Simply modifying the payload: in some rare cases, a server may simply skip token verification entirely and trust the data in the payload. Although I haven't seen this personally, I've read about it happening in the wild!
  • Switching RS to HS: there's a flaw in some older JWT libraries where you can trick an application expecting tokens signed with asymmetric cryptography into accepting a symmetrically signed token instead. The symmetrically-signed token that ends up being used is actually the public key, which can often be obtained or reused in some way from their HTTPS key.
  • Timeout not enforced, so JWT tokens with an iat remain valid forever.

Primary defense

The best fix for JWT flaws is to use a widely-used, reputable JWT library for all JWT operations.

Authorization issues / IDOR

Authorization is the process of verifying whether an authenticated user has access to a specific user's data. A common authorization-related vulnerability is known as Insecure Direct Object Reference (IDOR). For example, in an API for an invoicing application, we might have an endpoint used to fetch invoice details:

/api/v1/invoice/?id=12

The id parameter is the identifier of the invoice to be returned. If this endpoint is properly protected, I should only be able to retrieve details of invoices belonging to me. For example, if I created an invoice with ID 12, it should return the data. If I try to access an invoice I didn't create by viewing /api/v1/invoice/?id=13, it should return an error.

If I'm able to change the identifier to view other users' invoice details, that's a vulnerability known as IDOR.

To counter IDOR problems, many APIs today use UUIDs as object identifiers. A UUID looks like this:

f1af4910-e77f-22eb-beb2-0242ac131112

It's important to note that using UUIDs as identifiers is not a valid method of fixing IDOR issues. In fact, the UUID RFC specifically calls this out:

Do not assume that UUIDs are hard to guess; they should not be used as security capabilities (identifiers whose mere possession grants access). A predictable random number source will make this worse.

While it's recommended to use UUIDs as object identifiers instead of integers, they should never be used as the sole defense against IDOR attacks.

Primary defense

Authorization issues are usually hard to detect through automated means. The codebase's structure should be set up in a way that makes it difficult to make authorization mistakes on specific endpoints. To achieve this, authorization checks should be implemented as high up in the stack as possible — potentially at the class level or using middleware.

Undocumented API endpoints

It's common to encounter situations where the API you're attacking has no documentation (or at least none available to you). It's also fairly common for an API with documentation to have endpoints that go beyond what's documented. Sometimes the mere existence of these endpoints can be a security problem — for example, an endpoint might be intended for administrative purposes and allow you to perform administrative tasks as a limited-privilege user. In other cases, these endpoints may represent vulnerabilities simply because they haven't been tested as thoroughly as the ones that are easy to find.

Here are a few techniques we can use to discover undocumented endpoints:

  • Use the application that interacts with the API and capture the traffic. Probably the most commonly used tool for this today is Burp Suite.
    • Configure Burp Suite to proxy the application's traffic
    • Use all of the application's functionality
    • Review the endpoints used by looking at the Target tab.
  • Watch for errors. Many APIs return sufficiently detailed errors to enumerate undocumented endpoints and parameters. For example, sending an empty POST request to /api/v1/somestring might trigger an error saying something like Invalid route, valid routes are [/users,/invoices,/orders].
  • Analyze client-side JavaScript. If you're aware of an application that interacts with the API, you can analyze the JavaScript in that application to compile a list of API endpoints that might be accessible.

Primary defense

Having a solid, structured method and process for documenting API functionality can save a lot of headaches down the road. Swagger (OpenAPI) is a great standard for exactly this. In addition, it's best to plan API functionality through documentation before writing code, rather than the other way around.

Different API versions and their vulnerability

When an organization releases an API, it may interact with many different applications. If the API is updated at any point, this can cause breaking changes for one or more of those applications. For this reason, multiple API versions are often implemented as a way to keep supporting older API schemas while also updating the API over time for newer users.

It's worth testing all versions of the API. Older versions may still have security issues that have since been fixed in a newer version, while newer/latest/beta versions may introduce new security issues.

A common API versioning scheme:

/api/v1/
/api/v2/
/api/v3/
/api/alfa/

It's always worth checking non-production route names, such as:
Types of web API, SPA, and JWT vulnerabilities in 2022

Primary defense

API versions can be maintained with a defined lifecycle. For example, when you release version 2 of an API, you can notify your users that version 1 will reach end-of-life (EoL) and be discontinued on a specific date in the future. Pre-release/beta versions should only be made publicly available once they've been thoroughly tested for security issues.

Rate limiting

In most cases, APIs have no protection against the number of requests a user can make. This is called «lack of rate limiting» and occurs when an attacker can call an API thousands of times to trigger some unintended behavior. The server will attempt to fulfill each of these requests, and this can potentially:

  • DoS the server by overwhelming it with requests
  • Allow an attacker to quickly harvest sensitive user information, such as user IDs, usernames, emails, etc.
  • Bypass authentication by brute-forcing the login API
  • Fill up a victim's mailbox by brute-forcing a function that sends the victim an email/SMS.

Let's look at an attack scenario where there's no rate limiting on an API endpoint that verifies credentials:

GET/POST /api/v1/auth?login=intellect.icu&password=mypassword

Normally you wouldn't see a password sent in a GET request like this, but for the purposes of this demonstration let's assume you do. To brute-force the password at the endpoint above, an attacker can use dedicated tools. These let you configure various kinds of brute-force attacks, but for this example we'll feed it a simple password list.

Within a few seconds, Intruder will send hundreds of API requests, trying a different password with each request.

GET/POST /api/v1/auth?login=intellect.icu&password=somepassword1
GET/POST /api/v1/auth?login=intellect.icu&password=somepassword2
GET/POST /api/v1/auth?login=intellect.icu&password=somepassword3
GET/POST /api/v1/auth?login=intellect.icu&password=somepassword4

 . . .  

GET/POST /api/v1/auth?login=intellect.icu&password=true password

Since there's no rate limiting, the server will happily provide responses to all of these requests, and the attacker can keep trying different passwords as fast as possible until the correct one is found.

To defend against rate-limiting flaws, an application should set a limit on how often a user can call the API within a given period of time. The exact limit set will depend on the use case of that API or endpoint.

Primary defense

Rate limiting can be implemented in various ways: per account, per IP address, per endpoint, application-wide, and so on. Some reverse proxies can also be used to implement application-wide throttling without additional development work. The exact implementation you'll use will depend on your specific application's requirements.

Race condition

A race condition is when two or more requests are sent to an API within the same millisecond. If the API has no mechanism to handle this scenario, it can cause the API to process the requests in an unintended way.

A potential race-condition attack scenario can arise when discounts or promo codes are used in a vulnerable e-commerce application.

POST /api/v1/discount 

{ "code" , "cupon" , "amount" : "15" }

 

After configuring the attack in Turbo Intruder, an attacker can send multiple concurrent requests to the API to redeem this promo code.

 Types of web API, SPA, and JWT vulnerabilities in 2022 

If the API doesn't invalidate the promo code immediately upon receiving the first concurrent request, the discount amount could be doubled, tripled, and so on.

This attack is called a «race condition» because it's a race between how quickly the attacker sends requests to modify a resource and how quickly the application updates that particular resource.

Primary defense

Unfortunately, fixing race-condition issues often means a performance hit. Mitigation techniques for race conditions include using locks and other thread-safe features. Most programming languages have built-in thread-safety functionality, but it usually has to be specified manually.

XXE injection

XXE stands for XML External Entity, and this injection vulnerability can be tested anywhere an API is used to process XML data. SOAP APIs can also be vulnerable to XXE injection, since they're built on XML.

An API endpoint that uses XML looks something like this:

Types of web API, SPA, and JWT vulnerabilities in 2022 

An XML document uses entities to represent a single piece of data, and the Document Type Definition (DTD) is used to define the entities that will be used, the document's structure, and so on.

XXE injection refers to cases where an attacker injects custom external entities that fall outside the specified DTD. Once these external entities are parsed by the API, it may allow the attacker to access the application's internal files, pivot to SSRF, exfiltrate sensitive data to an attacker-controlled domain, or DoS the server.

Here's an example of a request in which the attacker has injected a custom external entity named xxe, with the goal of retrieving an internal file:

 Types of web API, SPA, and JWT vulnerabilities in 2022

If the API allows a standard XML parser to process the data, this injected external entity will be processed by the application and will return the contents of /etc/passwd to the attacker.

Primary defense

Make sure the XML parser being used is configured not to parse XML entities.

Content-type switching

Even though an API may use JSON as its data format for communication, the underlying server/infrastructure may still accept other data formats, such as XML. So when you see an API with a content-Type of application/json, you can still try testing for XXE by switching its value to Content-Type: text/xml

For example, if the API uses JSON:

POST /api/v1/user HTTP/1.1 Host : intellect.icu
 Content-type : application / json

It can be modified to send XML data as follows:

 Types of web API, SPA, and JWT vulnerabilities in 2022

Primary defense

This bug really only exists in frameworks designed to accept multiple formats, where each format maps to an object type. In these cases, frameworks usually have a way to whitelist the available formats. It should be noted that as of 2021 this is fairly rare.

HTTP methods

APIs typically support various types of HTTP methods. Some of the common ones are GET, POST, PATCH, DELETE, and OPTIONS. If a GET request is sent to an endpoint where the application expects a POST request, this can cause the application to respond in the most unexpected ways.

Take CSRF as an example. CSRF (Cross-Site Request Forgery) is when an attacker tricks a victim into sending a request that can trigger state-changing actions on the victim's account. These changes can range from modifying the victim's personal data to, in some cases, even changing the victim's password, leading to full account takeover.

A normal request to change a victim's personal data might look something like this:

  Types of web API, SPA, and JWT vulnerabilities in 2022
 
  
  
  
Looking at the request above, we might conclude that a CSRF attack is not possible, since the csrftoken value is sent in the request body. However, if the API doesn't restrict which HTTP methods can be used for this request, an attacker can bypass this protection by sending the request using the GET method.
GET /api/v1/user?name=some&email = some@example.com&location=mars
 host : intellect.icu 

In addition, sometimes the server simply doesn't validate the CSRF token, or accepts the request even if the CSRF token parameter is missing entirely from the request.

Another common vulnerability in REST APIs is when permissions have been correctly applied to an endpoint, but only for one HTTP verb. For example, you might not be able to GET other people's records, but you can edit their records using PATCH.

Primary defense

Specify HTTP verbs on routes manually. Don't use verbs unnecessarily, and make sure all specified endpoints have the same controls applied across all verbs. Some frameworks do this automatically, others don't.

Injection vulnerabilities

Backend injection flaws, such as SQL injection, RCE, command injection, and SSRF, can exist in APIs just as they exist in ordinary web applications. Whenever any injected data is passed directly to an interpreter on the backend, this leaves room for an attacker to send targeted queries and commands to access internal data or execute arbitrary code.

For example, let's test for SSRF using the following API request:

POST /api/v1/user
 host : intellect.icu
{"avatar" : "https://www.example.com/me.jpg" }


An attacker might try to exploit SSRF via the avatar field by sending a request like this:
POST /api/v1/user
 host : intellect.icu

{  "avatar" : "https://localhost/admin " }

     
If the backend interpreter doesn't properly validate the avatar value, this could allow an attacker to exploit SSRF and successfully access internal data.

Similarly, an attacker might also try to perform SQL injection if the data in this request isn't properly validated:

POST /api/v1/orders
host : intellect.icu
{ "offset" : 0 , "limit" : 10 , "scope" : "all" , }
  
  
  

Looking at the request body, we can get a hint that these parameter values are sent to a backend database interpreter. As a result, we can try performing SQL injection by changing these values to targeted queries:

POST /api/v1/orders
host : intellect.icu
{ "offset" : 0 , "limit" : 10 , "scope" : "SELECT sleep (100)" , }
  
  
  
Again, if the interpreter doesn't properly validate these values, this can lead to SQL injection, where an attacker can trigger a time delay in the database and even exfiltrate sensitive data.

Primary defense

Fixing injection vulnerabilities in APIs is essentially the same as fixing injection vulnerabilities in web applications. SAST/DAST scanners can help with this, but secure development practices are the best solution. Parameterize SQL queries, use ORMs and reputable libraries where possible, and never trust user input!

See also

  • [[b3]]
  • [[b6242]]
  • [[b7719]]
  • [[b5982]]

See also

created: 2021-08-11
updated: 2026-03-09
128



Was this answer useful?
Choose a quick rating so we can improve the next answer for you.
How satisfied are you?


Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "information security - Cryptography and Cryptanalysis. Steganography. Information protection"

Terms: information security - Cryptography and Cryptanalysis. Steganography. Information protection