When you’re building a microservices-based application, it’s understood that application programming interfaces (APIs) are what holds the whole thing together and makes it work. With more monolithic architectures, the APIs that are exposed might be less obvious, but if you have a web app, you almost certainly have some services running, meaning you have APIs that you should be testing for functionality and security. This post runs through a simple scenario to show how quickly APIs can proliferate in any architecture, quietly increasing your overall web attack surface – and how you can make sure you’re staying secure no matter what’s running under the hood.
Hidden architectural decisions: simple script or simple API?
One typical situation is where you need to deal with a separate process for doing some server-side operation. Say you have a full-stack JavaScript app with Node.js on the server that needs image manipulation functionality in one place, maybe to automatically generate image thumbnails. You could do this all in JavaScript but decide to use an existing Python script for performance and convenience. So as you’re prototyping your app, the simplest way is to call:
imgData
=exec('python3 resize.py "input-file.png" "output-file.png"')
copy
This makes an operating system call to execute the resize.py script, giving it an input file and expecting a resized output file. While this works well enough for a prototype, the approach has its downsides:
- Risk of command injection: If a malicious user is able to control the input or output file name and this data is not sanitized, the application could be vulnerable to OS command injection. In the worst-case scenario, an attacker may be able to execute operating system commands on your web server.
- Scalability and performance issues: The script runs on the same server as the main application, which could lead to performance issues in a high-load production environment, especially when handling concurrent requests.
- Limited access control: While likely not an issue for a small script, there is no easy way to control access to the resize operation itself or set rate limits. For a more complex script or executable with multiple operations and parameters, the only way to control access to each operation would be in the application logic.
A more elegant solution would be to put the image manipulation functionality in its own web service and define an API for it. The service would listen for calls at a specified URL, accept an input image, and return the resized image. It can run on the same server or somewhere entirely different, and you might use it by simply sending the input image to https://your-server-name/api/resize. This addresses most of the disadvantages of a simple local exec()
:
- There’s no direct risk of command injection since you’re not passing user input directly to a local script on the server (though you’re opening up a whole other can of security worms – more on that later).
- Easy to adapt, reuse and modify – once you have the API endpoint defined, how you implement the required operations is entirely up to you. The same endpoint can be used whether you have a single-purpose Python script behind it, decide to change the underlying technology or maybe set up a multi-purpose image manipulation service that adds more endpoints and features.
- The service is scalable independently of the main application. Depending on the load and business needs, you could run it on a single on-prem server, spread it across multiple containers in the public cloud or choose anything in between.
- You can define fine-grained access control and rate limiting for each API endpoint, setting up authorization, auditing, and logging as required.
In some cases, going the API route may be preferable for external reasons. For example, it could be that executing operating system commands using functions like exec()
is forbidden by security policy or simply disabled. Also, with more service-oriented application architectures, adding another service might simply be the natural thing to do.
Keep those endpoints where you can see them
The architectural benefits of going with a web service rather than calling a local process mean that APIs can crop up anywhere, even if the application itself doesn’t make heavy use of services. While in some ways it’s more secure than directly calling system commands from your web application, running an API comes with its own security challenges. There are also maintenance and infrastructure requirements – a server-side script doesn’t do anything until you call it, but once you’ve set up a service, you need to keep it running all the time.
The crucial security consideration for any public API is that it’s more exposed to attacks and abuse than your local script would be, contributing to the overall web attack surface of the application and potentially your entire organization. For all its benefits, the service-oriented approach brings added complexity and more potential for misconfigurations that could allow attackers to access functionality or data. For instance, the benefit of access control could be negated if authorization is set up incorrectly and the API accepts requests from unauthorized users. While this doesn’t seem a big deal for photo resizing (unless you happen to be using a particularly vulnerable graphics library), the same risk applies to APIs that serve up sensitive data such as customer details or financial information.
Once you have the initial API infrastructure scaffolding in place, adding another endpoint as part of ongoing development work is quick and easy – but testing is not, especially when it comes to security. Apart from checking for the many things that can go wrong with controlling access to the API itself, testing also needs to consider vulnerabilities in the service or application behind the API. After all, an API is merely an interface for communicating with underlying software, and if that software is vulnerable to attack, a malicious request sent via the API could result in a breach.
Scan everything, regardless of architecture
While the example given above is deliberately simplified, identical design choices are made every day for all sorts of applications and services. The decision isn’t always obvious even for a simple image manipulation library, let alone when designing access to a business-critical database. Depending on your specific use case, resources, and requirements, both options can be valid, and both have their own pros and cons to consider. While going the API route is the trendy thing to do, you may need to consider external factors like latency, data throughput, and potentially also cloud costs.
But whatever you decide works best under the hood, you still need to ensure that the resulting application is secure and remains so. To do this systematically and independently of all the different architectures, web technologies, and programming languages used across your web environment requires an automated testing solution that can cover your entire attack surface, including APIs. In practice, dynamic application security testing (DAST) is the only approach that can achieve this at scale, and an enterprise-grade vulnerability scanner with support for popular API types is the right tool to get the job done. Set up correctly and combined with workflow integrations, a quality DAST solution will help keep vulnerabilities at bay regardless of your current and future application architecture – so your developers can focus on innovation.