We are very much in the age of APIs. From widely-used single-purpose products like Slack to cloud-based solutions like Amazon Web Services (AWS) and Microsoft Azure, APIs are used to drive business processes in all kinds of industries, every day. For tech companies, whether you’re doing a monolithic back-end, containerized microservices, or serverless architecture, the most common way to build applications today is to create a backing API with a decoupled front-end. In some ways, as technologists, we have gotten quite good at securing APIs. With that said, certain aspects of them are still overlooked more often than not.
1. Rate-Limiting on the Authentication Route
I’m using the term rate-limiting in a broad sense here, including any effective combinations of dynamic throttling, lockouts, and anti-automation (think CAPTCHAs). Traditional server-rendered web apps once had the same issue. A lack of rate-limiting on the login page was an uncommon find. We got increasingly consistent with this over the years. Eventually, we started rate-limiting login forms more often than not. It seems that we have forgotten these lessons in the translation to RESTful APIs though, as I rarely see it. This roughly separates APIs into three groups:
- Those who use a third-party auth provider like Okta or Auth0.
- Those that only accept some sort of high-entropy generated API key.
- Those who are wide open to abuse by credential stuffing, password spraying, and brute-force attacks.
You don’t want your API to be in the latter group.
If you’re taking individual user credentials (username, email address, password) on your API, you should be using anti-automation countermeasures. CAPTCHAs can work just as well on a RESTful endpoint as they do on a traditional web app. It just means that your API needs to validate the CAPTCHA submission.
2. Strict Content-Type Enforcement
If you allow the Content-Type header on the request to be any valid mime type, there are a few things that can go wrong:
- Setting the content-type header to application/json will trigger a CORS preflight on an XHR or Fetch request. In the context of an API that uses cookie-auth or browser-originating HTTP Basic, Digest, or NTLM auth, the text/plain content-type allows an attacker to avoid a CORS preflight. This means they can potentially make cross-site request forgery (CSRF) attacks against your API. Allowing text/plain also enables a standard HTML
<form>element to be submitted in such a way that the request body is a well-formed JSON object, which provides another way of achieving CSRF.
Note: Changes in 2020 to the default behavior regarding the SameSite cookie flag have made CSRF less exploitable on the most popular browsers. It is still possible when the victim uses an older browser, a GET request is modifying data, or the attacker can force the cookie to be reissued before performing the CSRF e.g. by triggering the authentication flow.
- Some frameworks have multiple request parsers and will parse based on the specified content-type. The obvious concern would be that you write an API with the intent that it accepts JSON requests, but an attacker has a way to hit an XML parser through it by setting the content-type to application/xml. Depending on the parser and default config, this may open up XXE attacks on your API even though there was no intention to accept XML.
- Even if your API is not subject to either of the above issues, handling requests with an incorrect content-type is an unnecessary increase in the tolerance of the application. I like to relate tolerance to physical locks. The more play there is in the physical mechanism, the greater the potential to manipulate it in an unintended way. Even without a known exploit today, allowing that increased tolerance for malformed requests to be processed by your API is at least a nominal risk that there is no good reason to accept.
The content-type header’s purpose is to tell the server what type of data is in the request body. For the majority of modern APIs, application/json is the only content type they intend to support. Manyf back-end frameworks don’t check this header at all. Some other frameworks support other types by default, even if the developer of the API never intended to support those inputs. Your API should only accept requests that have a content-type matching your expectation.
You should give the same consideration to the accepts header as well, since requesting other formats of output, such as text/html, can result in unexpected vulnerabilities. If the developer intends to return an error message in a JSON structure, they probably aren’t escaping or encoding HTML. So if an attacker can instruct the framework to return HTML instead of JSON, it would increase the potential for cross-site scripting flaws. This is uncommon enough that it doesn’t warrant a separate list item, but it’s worth mentioning.
3. High-Entropy Identifiers
API routes tend to be relatively intuitive to somebody with access to a few samples, and some knowledge of the entities that the API deals with. An extremely common pattern is to have /company/23/user/42 type patterns using route parameters. When the API uses guessable or sequential route parameters, they become easily enumerable. This makes it likely that an attacker will try enumerating other numbers in the sequence sooner or later. That shouldn’t matter, because you should have good authorization controls anyway.
A high-entropy value like a UUIDv4 or GUIDv4 provides a layer of obscurity. We know that obscurity alone is not sufficient for providing meaningful security. But that does not mean that obscurity has no value. There are several scenarios where a high-entropy identifier increases the difficulty of exploiting a flaw that is present. Here are a few examples:
- The API or the application consuming it is susceptible to a blind attack, such as cross-site request forgery (CSRF). An attacker will find it significantly more difficult to target a valid resource if they need to know its UUID. In contrast, if the identifiers were sequential integers, the attacker could brute-force them.
- Authentication or authorization checks are missing from a particular route. This is a bad flaw to have, and using unpredictable identifiers will not excuse it. But using UUIDs or GUIDs will decrease the likelihood and likely scale of successful exploitation of this flaw.
- The application consuming the API has a cross-site scripting (XSS) flaw. The value of the high-entropy identifiers is far less clear and more circumstantial in this case. More often than not, an attacker with a cross-site scripting flaw has a path to get what they want from the app. Addressing the flaw is critical to the security of the application and the data that it accesses. However, in some cases making the IDs more difficult to enumerate will raise the complexity of the attack. Even if it isn’t enough to stop the attack, it can make it noisier and more likely to be detected.
These practices are not the answer to critical issues like injection flaws. However, they highlight some core principles that will strengthen your API’s security posture. Don’t forget the lessons we, as an industry, learned from over twenty years of web application development. Be certain to use the standard HTTP headers effectively. Be rigid in your expectations for request structure. And be aware that APIs are a prime target for enumeration.
Checkout our other API security content below.