Lambda URLs Might Not Be a Good Idea
September 29, 2024
Disclaimer: While I have a huge interest in cloud computing, I am not a cloud security expert by any means. What is described in this blog post might not be completely accurate. Feel free to reach out to me if you notice anything incorrect, and I will be happy to update the post.
A few months ago, Maciej Pocwierz published a blog post that went viral after he received a huge AWS bill for an empty S3 bucket. I recommend you read the original blog post, but to summarize: a popular open-source tool was misconfigured and pointed to the wrong S3 bucket for backups. Naturally, the requests did not go through since the bucket was private. However, it turns out that AWS charges for unauthorized incoming requests as well. The last update to the blog post mentions that the S3 team was already working on a solution for this issue.
After reading the post, I started thinking that this might not be unique to S3 but could also happen to other AWS services that expose public URLs without any sort of protection against application-layer DDoS attacks or, more accurately, Denial of Wallet attacks (making malicious requests to serverless services with the purpose of inflating the bill of the owner). In the case of CloudFront distributions and Route 53, by default, they already provide DDoS protection for network layers 3 and 4. This is a already good starting point. For organizations that require an extra layer of protection, AWS Shield Advanced can be used to mitigate more sophisticated attacks.
The service I’m most interested in, however, is Lambda. With the release of Lambda URLs and the increasing popularity of Lambda monoliths, it seemed that one could simply bypass API Gateway. The advantages are obvious: no extra costs for API Gateway, handling routing with your preferred library, reduced latency, fewer configuration complexities, and so on. Lambda URLs are certainly an attractive feature. With SST v3, deploying a Lambda function with tRPC and a public URL is as simple as:
const trpc = new sst.aws.Function("Trpc", {
url: true,
handler: "index.handler",
});
Unfortunately, as with any architectural decision, there are trade-offs; and in this case, it’s a big one. Lambda URLs are a convenience feature rather than a security one. Lambda URLs don’t provide any form of protection. By default, the Lambda function can be triggered multiple times by a simple malicious script, opening the door for a Denial of Wallet attack or even a Denial of Service attack. Since API Gateway is not used, it’s likely that the Lambda will handle some sort of authentication logic. For each request, the Lambda might have to retrieve a token from the headers, verify its validity, and either return an error or proceed with the request. If the authentication strategy is not stateless and requires querying a database, the impact of an attack is even higher. Not only would you have to pay for Lambda usage, but also for database usage. In other words, a malicious attack might succeed in increasing the AWS bill little by little, while potentially affecting legitimate requests.
It’s true that for this to happen, the attacker would need to know the Lambda URL. Guessing the URL would probably be quite challenging since they are usually long and have several random characters. However, if it’s directly consumed by a client-side frontend application — a very common pattern — a simple look into the Network tab of the browser could expose the URL. Or you might have intentionally shared the URL publicly so that it can be consumed by other users.
You might decide that the risk is acceptable. In general, I think Lambda URLs are a very convenient feature and might be appropriate for some use cases. For critical APIs, however, I believe having an extra layer of protection is a good idea. The good news is that there are several solutions for this problem, and you can choose the one that best suits your needs.
The first option is to put a CloudFront distribution in front of your Lambda function. This reduces the likelihood of your Lambda URL being leaked. Furthermore, you can leverage some of the nice features of having a CDN. This pattern is not uncommon — OpenNext successfully uses it to protect the Server layer of a Next.js app. Naturally, this solution requires additional configuration. AWS provides guidance on how to implement this solution, including the use of Lambda@Edge to sign requests.
The second option is to use a REST API from API Gateway as a proxy. This doesn’t necessarily mean you need to have a separate Lambda for each endpoint. In fact, you can create a single route that forwards all requests to the Lambda function. API Gateway doesn’t inherently add additional protection, but by combining it with AWS WAF, you can make your API more resilient against application-layer attacks, as described in the AWS Best Practices for DDoS Resiliency Whitepaper. For instance, some of the AWS WAF rules offer protection against suspicious IP addresses, common vulnerabilities described in OWASP publications, and known malicious inputs. Even more, rate-based rules can really stand out, as requests that exceed a certain threshold can immediately be blocked.
It’s worth mentioning that these solutions are not mutually exclusive. In fact, you can combine them to achieve even more resilience: AWS CloudFront + AWS WAF + AWS API Gateway. However, most applications probably won’t need this configuration initially. The simplicity of a Lambda with a public URL would be replaced by extensive infrastructure configuration with this approach.
Conclusion
The whole point of this blog post is to highlight that AWS is an extremely flexible ecosystem, and for this reason, it’s unlikely that we’ll find a one-size-fits-all solution. This is not inherently bad; on the contrary, it allows us to choose the right solution for our needs without having to pay for additional features that are not strictly necessary. Nevertheless, it is important to be aware of the security implications of our decisions. AWS is not known for being as convenient as other simpler cloud providers where, with a single click, you can be confident that your app is immediately protected against most threats. Of course, this comes at a price. SST is doing an extraordinary job at abstracting some of the challenges of using AWS with a simple API. Still, the extra steps required to protect an application require a lot of effort and are prone to configuration errors. Therefore, I believe SST users could benefit from an option to activate some security defaults to protect their AWS resources (e.g., common WAF rules for all CloudFront distributions and REST APIs).