Let’s start with some background -
During the 2018 AWS Re:Invent conference, the company launched a ton of new features, and a few special ones which the serverless community has been waiting for. Specifically, the Runtime API and Lambda layers.
The Runtime API enables developers to build custom runtimes that integrate with Lambda to execute functions in response to events. By leveraging the Runtime API, developers can use binaries or shell scripts, and their choice of programming languages. Getting started with Runtime API is simple. When deploying a function using the Lambda management console or CLI, developers can provide their own runtime. During an invoke, Lambda will bootstrap the runtime code and communicate with it over Runtime API to execute the function code. As of now, there have been numerous runtimes already published, such as: C++, Rust, Bash, Crystal, Nim, PHP, different versions of Node.js, etc.
Lambda Layers are a new type of artifact that can contain arbitrary code and data, and may be referenced by multiple AWS Lambda functions at the same time. Lambda functions in a serverless application typically share common dependencies such as SDKs, frameworks, and now runtimes. With layers, developers can centrally manage common components across multiple functions enabling better code reuse. To use layers, developers need to put common code in a zip file, and upload it to Lambda as a layer. Once uploaded, developers can configure functions to reference it. When a function is invoked, the layer contents become available to the function’s code.
PureSec also unveiled its own AWS Lambda protection layer during Re:Invent 2018, and you can read more about this Here.
Security Considerations for Lambda Runtimes & Layers
The launch of Runtime API & Lambda Layers is a huge milestone for AWS Lambda and will most probably boost serverless adoption, as it opens up the platform and enables developers to innovate even more than before. Having said that, allowing developers to go wild might also introduce security challenges, which require careful consideration. Paul Johnston recently published his own take on custom runtimes in his blog, and my initial response was that layers most probably introduce code dependencies, oftentimes untrusted ones. So, developers should make sure they cover layers in their code analysis scans during CI/CD.
Since the Runtime API and Layers launch, I developed a new hobby - to look for new runtimes or layers, and see who comes up with the craziest idea. I noticed that Graham Krizek published a Bash shell runtime/layer: https://github.com/gkrizek/bash-lambda-layer - and this got me thinking about security…
Here’s what I was thinking -
“Attackers love shell command injections. Here’s a Lambda runtime, that’s written in Bash, runs Bash-based functions, and handles input in Bash…. how convenient...”.
In essence, this Bash runtime is an exploit waiting to happen. 50% of the tough part for the attacker, which is getting access to a shell interpreter was already handed on a silver platter.
We looked at some of the sample functions provided in the Github page, and decided to give it a try. We had to fix a few things that weren’t really working, and ended up with a function that is very similar to the original one - all it does is receive a name of an S3 bucket, and run the ‘aws s3 ls’ command on it.
In this specific sample, the way the code is written, and the fact that the developer is using the built-in Bash variable expansion - it isn't easy to find a working exploit for injecting additional commands, although it's probably not impossible. However, there’s a much simpler attack vector here. By using a [space] character as the value for the bucket’s name -
"bucket": " "
Invoking the function with this value, will cause the function to list all of the S3 buckets in the account. Definitely not the intended outcome!
A possible fix would be to encode Bash-sensitive characters by using the printf builtin command, and using the %q format string, which prints the associated argument shell-quoted and reusable as input.
As Paul Johnston correctly noted: “One very big note of caution is to say that you should only be reaching for a custom runtime in a “last resort” scenario” - and I definitely second that, with an emphasis on the security implications. Bringing in new runtimes, some of which are not easy to secure is asking for trouble.
Special thanks goes to Yuri Shapira from our team, who spent his morning battling Bash to help me with this post.