LAST UPDATED October 6, 2022
More Simple = Less API Attack Vectors
The bottom line is that every feature of your API is a potential attack vector. Simplifying your API can reduce your attack surface area, in turn allowing you to better focus your security efforts. The good news is that many of the recommendations in this article align well with general software engineering best practices – that is, if you are an engineer or application owner, you are likely already following many of these suggestions, and if not, then doing so may result in other benefits in terms of quality, reliability, and reduced maintenance costs.
Use Existing Standards and Conventions Whenever Possible
Whether you are communicating dates/times, telephone numbers, or authorization information, there’s likely a standard out there for it. Using an existing standard makes it more likely you can leverage high-quality standard libraries to parse and serialize that information, and lessens the likelihood of an attacker exploiting a loophole in a custom data format.
In addition to using well-defined standards, closely following the conventions of the various technologies in use by your application will help you avoid pitfalls of those technologies. For example, using the Authorization header for access tokens will signal to intermediate proxies that the HTTP transaction is authorized and therefore should not be cached.
Use a Stateless Authorization Model
Stateless authorization models, typified by the use of JWT tokens, greatly reduce the amount of state management your API server must perform in order to serve requests and manage sessions. With stateless authorization, the holder of a token (or other authorization mechanism) is allowed to perform the actions described in the token, and no more. This pattern of managing authorization data decreases the amount of session state transitions that can be potentially hijacked and misdirected for nefarious means by an attacker.
Note, even when using stateless authorization, it is still absolutely necessary to check permissions consistently on every endpoint.
As a final note on this topic, beware that JWT token attacks are a real thing. Although a combination of cryptographic fundamentals and defensive programming can effectively prevent JWT attacks, to be safe from these attacks, you should consider encrypting your JWT token as an extra layer of protection.
Deprecate Old Endpoints
More endpoints, more problems. Every endpoint you operate represents a potential attack vector that adversaries may seek to exploit.
With a public API under active development, it’s easy to accumulate endpoints over the years. Unintuitively, it can often be older, less used legacy or compatibility endpoints that serve as an entry point for attackers. One solution to a growing number of endpoints is to implement a deprecation and end-of-life policy, with the ultimate goal of disabling and deleting old endpoints that are no longer used. A WAF with API analytics capabilities can help you identify which endpoints are no longer used, and which clients are still actively using old endpoints.
Push Functionality to Clients When Possible
Years ago, certain clients such as web and mobile apps were heavily dependent on server-side applications for basic data processing. Now, however, greater computing power and thriving open source software ecosystems for these platforms means you can push many computation tasks to client applications, allowing your API to take on less presentation and display responsibilities. For example, instead of rendering HTML in your API (presenting a potential XSS vector), your API can present the raw information contained within the HTML, and require the client to render HTML appropriate for the application.
Note, this tip works best with new development as it can sometimes be very difficult to change existing clients.
Be Conservative About Returned Data
It is common for application engineers to default to including all available data whenever given an opportunity to expose information about a topic. This behavior stems from a well-meaning intention to present information as completely as possible and “future-proof” an API, making it applicable for applications that were not originally envisioned by the application owner.
However, this practice can backfire spectacularly from a security perspective when the data returned represents an info leak or otherwise functions as an attack vector. Being conservative in resource representations and only including data necessary for well-understood use cases not only decreases implementation effort, but it also presents less footholds for a determined attacker to understand, undermine, and exploit your API.
Reduce Variant Resource Representations
Variant resource representations is a fancy way of saying that your API allows more than one way to accomplish a task. This practice can take many different forms:
- Different endpoints/commands to create a resource vs. updating a resource (both ultimately resulting in the resource being persisted)
- Different endpoints/commands to create one resource vs. multiple resources in bulk
- Interfaces for query/sorting/filtering options that differ from one endpoint to the other, with the outcome ultimately being similar if not identical
- Client-specific endpoints (for example, optimized endpoints for serving data for a specific mobile app)
In all of the above cases, the variant representations (whether in the request or in the response) allow an attacker to potentially take advantage of inconsistent parameter handling and/or data handling logic.
To address this problem, identify the “least common denominator” of functionality needed in order to have a minimum number of data formats and conventions in your application. As an example, you could implement only a bulk-save functionality instead of having different endpoints for create-one, create-bulk, update-one, and update-bulk. Another example is replacing endpoint-specific parameters for implementing sorting, filtering, and querying rules, and applying a common convention for these parameters across all endpoints. Finally, if your API must support clients requiring highly optimized endpoints (such as a mobile app), consider using an API query/manipulation tool such as GraphQL to define and serve this data without having to create an entirely new endpoint.
Separate Endpoints into Read vs. Write Permissions
Enforcing a strict separation between read-only actions and mutative actions in your API endpoints will make it much easier to analyze the meaning behind traffic patterns and enforce permission structures, e.g., preventing read-only users from deleting data or limiting what data certain users can edit. If you are already using HTTP conventions, these write actions will align neatly with conventional HTTP methods such as PUT, POST, and DELETE.
Align Resource Boundaries to Permission Boundaries
To go one step further on the topic of separating endpoints based on permissions, breaking down your API endpoints into smaller resources based on your permissions structure can actually help simplify your application.
As an example, let’s take a hypothetical user management API endpoint that allows an authorized client to manage a resource containing the following fields:
- Username – not editable
- Phone number – editable by the principal user or any admin
- Hashed password – editable only by the principal user
- Admin – editable only by an admin
To edit this resource, a client must present a signed access token issued to a specific user. What kind of authorization logic is necessary to fulfill a request to update this resource?
- If the access token does not belong to an admin, and the username on the token does not match the username on the resource, reject the request.
- If the access token does not belong to an admin, but the username on the token does match the username on the resource, allow the update as long as only the hashed password and phone number are the only fields that are changed.
- If the access token belongs to an admin and either no password change is requested, or the username in the resource matches the username on the token, then allow the update as long as the username field is not changed.
Obviously this logic, while not exactly trivial, can be capably expressed in every modern programming language. However, it is complicated enough that it leaves opportunities for subtle errors in the implementation. If any one of these logical determinations are calculated incorrectly, it can allow an attacker to execute a privilege escalation attack.
Alternatively, you could break this resource up into smaller pieces:
- Base user record – read-only resource containing the username and other static identifier fields
- Profile information – phone number and other informational fields editable by the principal user or any admin
- Login credentials – hashed password, 2FA credentials, and other authorization fields editable only by the principal user
- Admin authorization – authorization granting the user to act as admin, editable only by an admin
You can see how it would be much simpler to enforce authorization logic for the above sets of endpoints. It becomes a clear exercise of validating the presented access token, and verifying the access token is allowed to access or modify the identified resource. It is no longer necessary to define complicated contingencies and examine individual fields in the request or the stored values in the database in order to determine whether the operation should be allowed.
This may seem pedantic or contrived at first glance, but determined attackers will seek out complicated endpoints such as this one as targets and attempt every possible combination of unexpected inputs in order to trick the system into behaving in unexpected ways. Aligning your resources to your permission structure will help keep your implementation simple.
Implement Generic Audit Logs
“Insufficient Logging and Monitoring” is one of the OWASP API Security Top 10 vulnerabilities. You can miss logging or implement it incorrectly if it is implemented in an ad-hoc manner within each individual endpoint. To avoid this pitfall, you can implement logging in a generic way.
For example, if you follow the advice above, you will have endpoints that align cleanly to the permission structures of your HTTP application and that use HTTP conventions to describe the action being performed. This combination of attributes implies that the HTTP method and endpoint path in conjunction will describe, to a great degree, the action being requested. Combining that information with other standard information about the request and response (e.g., the username identified in the access token, the status of the response, and timing information) will give you a pretty decent high-level overview of the action going through your application. The power of this approach is that it can be implemented without any endpoint-specific logging, meaning less risk of any individual endpoint implementing logging incorrectly.
Note, while generic audit logs are a good layer of protection to give you insight into your HTTP application activity, they don’t need to replace more specific logging within individual endpoints. Generic audit logs can function as defense-in-depth that complements existing endpoint-level logging you already have in place.
Use a Web Application and API Protection Solution
A good Web Application and API Protection (WAAP) solution can be an invaluable tool to help give you visibility into which parts of your application could benefit most from additional security measures based on detected attacks, error rates, and overall traffic. And while security features such as rate limiting and secure response headers can be implemented directly in the source code within your API, a next-gen WAAP will allow you to implement these features in a way that adds no complexity to your application code and can be objectively audited. Using a WAAP tool gives you the power to implement instant protection, even in the case of a zero-day exploit, without the need to go through an expensive, time-consuming, and distracting development and release cycle. Using the right tool for the job in this instance is a great way to decrease the complexity of your API and establish strong confidence that your security measures are implemented consistently.
Learn more about API security in our recent 20-minute webcast.