Part 1 of 4 in the Lambda Security Series
“ It’s serverless. There’s no server to hack. ”
I hear this in almost every architecture review. It is one of the most expensive misconceptions in cloud security.
AWS does run the servers. AWS patches the host operating system, isolates the execution environment, and keeps the control plane healthy. That part is real, and it is genuinely better than managing your own fleet. But everything that actually gets your account breached still belongs to you: the code, the IAM permissions attached to the function, the environment variables, the network configuration, and the events that trigger the function. None of that is AWS’s job. All of it is yours.
This is the part of the shared responsibility model that teams skip when they move to Lambda. They assume “managed” means “secure.” It does not. It means the attack surface moved, and most of it moved closer to your code.
Where the Attack Surface Actually Lives
A Lambda function is a small unit of compute with an IAM role bolted to it, a set of environment variables, optional network access, and one or more triggers. Each of those is a place where things go wrong.
Overprivileged execution roles
Every Lambda function assumes an IAM role when it runs. That role is the function’s identity inside your account. Whatever the role can do, the function can do, and whatever the function can do, an attacker who finds a flaw in your function code can do.
The failure mode is always the same. A function needs to read from one S3 bucket. Writing a scoped policy takes ten minutes, so someone attaches **_AdministratorAccess_** instead and moves on. Now a single injection bug, a vulnerable dependency, or a server-side request forgery in that function is no longer a function-level problem. It is an account-level compromise. This is the same blast-radius mistake that turned a single misconfigured component into the Capital One breach, and serverless does nothing to prevent it. If anything, it makes the mistake easier to ship, because the role is invisible in the function’s day-to-day behavior.
Least privilege is not a nice-to-have in serverless. The execution role is the primary security control.
Secrets in environment variables
Environment variables feel like a natural place to put configuration, so people put secrets there too:
DB_PASSWORD=MyPr0ductionP@ss!
STRIPE_SECRET=sk_live_26PHem9AhJZvU623DfE1x4sd
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Lambda environment variables are not a secret store. By default, anyone with **_lambda:GetFunctionConfiguration_** on the function can read every variable in plaintext. That permission is extremely common. It is included in read-only and developer roles, it shows up in CI pipelines, and it is exactly the kind of low-value permission that gets handed out without much thought. The moment a secret lands in an environment variable, its security depends entirely on nobody ever holding a fairly ordinary read permission.
AWS Secrets Manager and SSM Parameter Store exist precisely for this. The correct pattern is to store a reference, such as a Secrets Manager ARN or an SSM parameter path, and resolve the real value at runtime. The variable then contains a pointer, not a credential.
Public function URLs and wildcard CORS
Lambda function URLs are a fast way to put an HTTP endpoint in front of a function without an API Gateway. They are also a fast way to put a function on the public internet by accident.
A function URL has an **_AuthType_**. Set it to **_AWS\_IAM_** and callers must sign their requests. Set it to **_NONE_** and anyone with the URL can invoke the function: no authentication, no authorization, just a POST request. Pair that with a wildcard CORS policy (**_AllowOrigins: \*_**) and you have published a cross-origin callable, unauthenticated HTTP endpoint that runs your code and assumes your execution role. Attackers scan for these continuously.
Public resource policies
A function also has a resource-based policy that controls who is allowed to invoke it. A statement with **_Principal: \*_** and no conditions means any AWS principal, in any account, can call your function. It is the serverless equivalent of leaving a database open to the world, and it is easy to create when you are wiring up cross-account access and reach for a wildcard to make the error message go away.
Deprecated runtimes
When AWS deprecates a runtime, that runtime stops receiving security patches. The function keeps running, which is exactly why this risk is so easy to ignore. Nothing breaks. The code just sits on an unpatched base image, accumulating known vulnerabilities that will never be fixed.
The schedule is real and it moves every few months. As of May 2026, the current supported runtimes are: Python 3.14 and Python 3.13 and 3.12 and Python 3.11 and Python 3.10, Node.js 24 and Node.js 22, Java 25 and Java 21 and Java 17 and Java 11 and Java 8, .NET 10 and .NET 9 (Container Only) and .NET 8, Ruby 4 and Ruby 3.4 and Ruby 3.3, and the OS-only **_provided.al2023_** and **_provided.al2_**. The recently deprecated list is long and growing: Python 3.9 reached deprecation on December 15, 2025, Ruby 3.2 on March 31, 2026, and Node.js 20 on April 30, 2026. Python 3.8 has been deprecated since October 2024.
There is a subtlety that keeps deprecated runtimes alive far longer than they should be. AWS extended the “block function update” date for most legacy runtimes to March 3, 2027. Until that date, a function on a deprecated runtime still runs and can still be updated, so there is no hard forcing function. Teams see no error and assume there is no problem. The vulnerabilities are still there.
Event-data injection
A Lambda function rarely sits behind a single, well-understood entry point. It receives events from S3 notifications, API Gateway, SNS, SQS, DynamoDB streams, EventBridge, and more. Each source is a separate trust boundary, and each one can carry untrusted input straight into your code. The OWASP Serverless Top 10 lists event-data injection as the top serverless risk for this reason. The attack surface is wider than a traditional web app because the entry points are less obvious, and a successful injection inherits whatever the execution role grants. Overprivileged roles and injection are the same problem viewed from two angles.
This Has Already Happened
Serverless incidents are underreported, because most organizations never disclose them. Two public cases are worth knowing because they map directly to the risks above.
In 2022, researchers at Cado Security documented Denonia, described as the first malware specifically targeting AWS Lambda. Denonia is a Go wrapper around the XMRig cryptominer, built to run inside the Lambda execution environment. Cado noted that the deployment method was not confirmed, with the most likely explanation being stolen or leaked AWS access keys used to create the functions. The lesson is not the miner itself. It is that valid credentials plus the ability to create Lambda functions is enough to turn your account into someone else’s compute.
OWASP maintains ServerlessGoat , a deliberately vulnerable Lambda application used for training. It is a small document-to-text converter that takes a URL and runs it through a shell command. Because user input flows directly into that command, it is vulnerable to OS command injection. The documented exploit chain uses that injection to read the function’s own source code, dump its environment variables, and exfiltrate data from the DynamoDB table the function can reach, all through the function’s execution role. It is a clean, reproducible demonstration of how one injection bug plus one overpermissive role becomes data loss.
Why This Is Hard to See
None of these problems announce themselves. A public function URL returns a normal response. A function with **_AdministratorAccess_** behaves exactly like one with a scoped role until the day it is abused. A deprecated runtime runs without warnings. A secret in an environment variable works perfectly.
The other half of the problem is scale. A real account has dozens or hundreds of functions, spread across regions, owned by different teams, deployed by different pipelines. Reviewing them by hand means opening each function, reading its resource policy, inspecting its execution role, decoding its environment variables, and cross-referencing the runtime deprecation schedule, one function at a time, in every region. Nobody does this consistently, which is why the gaps persist.
That is the visibility problem, and it is the problem this series is about.
What’s Next
Before you read Part 2, you can find the most dangerous misconfiguration in a few seconds. This lists every function URL in a region and its authentication type:
for func in $(aws lambda list-functions \
--query "Functions[*].FunctionName" --output text); do
URL_CONFIG=$(aws lambda get-function-url-config \
--function-name "$func" 2>/dev/null)
if [$? -eq 0]; then
AUTH=$(echo "$URL_CONFIG" | grep -o '"AuthType": "[^"]*"')
echo "$func: $AUTH"
fi
done
Any line that shows **_AuthType: NONE_** is a function anyone on the internet can invoke.
In Part 2, I introduce lambda-security-scanner, an open-source tool that runs nineteen checks against every function in your account, scores each one from 0 to 100, and tells you exactly what is wrong and how severe it is. In Part 3, we map every finding to ten compliance frameworks and walk through the AWS CLI commands that fix each issue. In Part 4, we go past configuration into the application layer: the code itself, its dependencies, and the credentials it holds.
Sources:
- OWASP Serverless Top 10 (https://owasp.org/www-project-serverless-top-10/)
- Cado Security: Denonia, the first malware specifically targeting Lambda (2022) (https://www.cadosecurity.com/blog/cado-discovers-denonia-the-first-malware-specifically-targeting-lambda)
- OWASP ServerlessGoat (https://github.com/OWASP/Serverless-Goat)
- AWS Lambda runtime deprecation policy and schedule (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html)


Top comments (0)