The Myth of the Invisible Function
Here's what I keep seeing in cloud security reviews: teams deploy Lambda functions, wire them up behind API Gateway or an ALB, and then mentally file them under "handled." They've got WAF rules on the gateway. They've got a VPC. They feel good. And that feeling is exactly what's going to get them compromised.
Lambda functions are not firewalled. Not really. The gateway in front of them is a traffic router with some optional filtering bolted on — it is not a security perimeter. The moment an attacker gets a payload past that layer, or finds a trigger that bypasses it entirely (and there are several), your function is exposed naked to whatever that event carries. No host-based firewall. No network stack in the traditional sense. No kernel you control. Just your code, the runtime, and whatever IAM permissions some well-meaning engineer assigned three sprints ago without ever expecting to revisit.
Let me walk you through how this actually breaks down in production, because the theory is one thing but the specific failure modes are what matter.
Event Injection Is the New SQLi (And It's Just as Boring to Prevent)
Event injection is probably the highest-frequency Lambda vuln I've encountered in the wild, and it drives me nuts precisely because it's so preventable. The attack surface is the event object itself — the JSON blob your function receives from API Gateway, SQS, SNS, S3, whatever. Developers trust that input implicitly. They treat it like a system message rather than user-controlled data. It is user-controlled data.
Take the canonical API Gateway case. A request hits your Lambda via a POST /webhook route. The event object contains body, headers, queryStringParameters, and a dozen other fields. Your function deserializes the body and passes it to a subprocess call, a database query, or — and I've seen this more than I'd like to admit — directly into an eval() or a templating engine. API Gateway passes the raw HTTP request body into the event without modification unless you've explicitly configured a request validator and body mapping template. Most teams haven't. The WAF on the gateway inspects HTTP — it doesn't understand your business logic or your internal event schema.
But API Gateway is the obvious case. The nastier scenario is indirect triggers. An attacker doesn't have to hit your API. If your Lambda processes S3 object metadata, they upload a malicious file with a crafted filename. If it processes SQS messages from a queue that has any external-facing producer, they inject through that. If it subscribes to SNS topics that accept unvalidated input, same story. Your "internal" Lambda that never faces the internet directly is still reachable — it just has a longer attack path.
The fix isn't elegant. It's boring input validation on every field of the event object before you touch it. Use a schema validation library — JSON Schema validation with something like ajv in Node or pydantic in Python. Validate at the entry point of the handler, not downstream. Treat the event object exactly like you'd treat an HTTP request from an untrusted client, because that's what it is.
The IAM Execution Role Situation Is Actually Embarrassing
Hot take: the average Lambda execution role I see in production environments would make a penetration tester weep with gratitude.
IAM execution roles are how your Lambda authenticates to other AWS services. The credentials are injected into the runtime automatically via the metadata service — your function calls sts:AssumeRole implicitly and gets temporary keys that are valid for the duration of the execution context. This is fine in principle. In practice, teams attach AdministratorAccess or a close cousin and ship it.
I was doing a review for a mid-size fintech last year. Lambda function processing payment webhooks. Execution role had s3:* on *, full dynamodb:*, and — I'm not exaggerating — iam:PassRole. That last one means that if an attacker achieves code execution inside that Lambda, they can pass an IAM role to any service that accepts it. That's privilege escalation to essentially arbitrary AWS actions depending on what roles exist in the account. The whole account, not just that function's resources.
The CISSP framework calls this least privilege. In the serverless world it translates directly to: each Lambda function gets a unique execution role, scoped to the exact resources it needs with the exact actions it needs on those resources. That means using resource ARNs, not wildcards. That means separate roles per function, not one shared role for the whole application. It means running Prowler or Checkov against your IaC templates before deployment — both tools have specific checks for overpermissive Lambda execution roles and will catch the worst offenders automatically.
And no, the VPC placement doesn't help here. IAM credential theft via code execution works regardless of network topology.
SSRF Inside a VPC-Attached Lambda Is a Special Kind of Terrible
Let's talk about the scenario that I think is genuinely underappreciated: SSRF in a Lambda function that's attached to a VPC.
Teams attach Lambda to a VPC for two reasons — to reach private RDS instances or ElastiCache clusters, and because it feels more secure. The second reason is the dangerous one. The sense of security that VPC placement provides is partially real and substantially false. Yes, your function can now reach private resources. But you've also just given a successful SSRF exploit access to your private network.
Here's the thing about the AWS metadata endpoint inside a Lambda: it's not 169.254.169.254 the way it is on EC2. Lambda uses the AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable to point to an internal endpoint for credential retrieval. That endpoint is different from the EC2 IMDS and IMDSv2 mitigations don't apply in the same way. If your function makes outbound HTTP requests based on user-supplied input — a URL fetcher, a webhook dispatcher, a preview generator — and you don't have strict SSRF protections, an attacker can probe your VPC internals and hit internal services that assume they're unreachable from the outside.
The credential retrieval endpoint in particular: if it's reachable via SSRF within the execution environment, you're handing over temporary AWS credentials. Those credentials have the permissions of the execution role. We've come full circle back to the IAM section, haven't we.
Practical mitigations: validate and allowlist URLs before making outbound requests, block RFC 1918 address ranges in your HTTP client configuration, and use a dedicated egress proxy if your threat model warrants it. AWS doesn't do this for you.
Cold Starts, Credential Caching, and the Footgun You Didn't Know You Had
This one is subtle and I almost missed it the first time I encountered it in a real environment.
Lambda execution contexts persist between invocations when the runtime is warm. This is a performance feature — it saves your database connections, your SDK clients, your initialized state. But it also means that credentials and secrets cached in memory persist across invocations from potentially different users or requests. If your initialization code — the code outside the handler function — fetches credentials from Secrets Manager or Parameter Store and caches them in a module-level variable, those credentials sit in memory for the lifetime of that execution context.
In most cases this is fine and expected. Where it becomes a problem is when your cold start initialization makes assumptions about the execution context that don't hold across all invocations. I've seen cases where multi-tenant functions cached a tenant-specific secret during initialization based on the first invocation's context, then served all subsequent invocations from that same cache. That's a data isolation failure hiding inside a performance optimization.
The fix requires intentional design: understand what you're caching and why, implement TTL-based rotation for cached credentials even in warm contexts, and be extremely careful about any initialization code that makes decisions based on event-level data. The Lambda programming model blurs the line between "module setup" and "request handling" in ways that other compute models don't, and that blurring has security implications your developers may not have thought through.
Environment Variables Are Not a Secrets Manager
I don't care that it's convenient. Stop putting your database passwords in Lambda environment variables.
Lambda environment variables are encrypted at rest using a KMS key — by default the AWS-managed key, which means every IAM principal with basic Lambda access in your account can decrypt them. They're visible in the Lambda console to anyone with lambda:GetFunctionConfiguration. They appear in CloudTrail API responses. They show up in infrastructure-as-code templates that get committed to Git repositories where they'll live forever in the commit history.
AWS Secrets Manager and Systems Manager Parameter Store (SecureString parameters) exist precisely for this reason. Yes, they add latency and complexity. The latency is milliseconds and the complexity is writing twenty extra lines of initialization code. That tradeoff is not a real argument against using them.
The CloudWatch angle here is worth mentioning separately: your Lambda functions are logging to CloudWatch by default, and unless you've explicitly scrubbed your log output, there's a reasonable chance that secrets are flowing through your log streams. I've seen API keys, JWT signing secrets, and database connection strings in CloudWatch logs because a developer added a debug line that printed the entire config object. CloudWatch log groups are accessible to anyone with the right IAM permissions, and CloudWatch data can be exfiltrated via logs:GetLogEvents — a permission that often gets granted liberally because "it's just logs."
Lambda Layers and the Supply Chain You're Not Watching
This is the one that keeps me up at night more than the others.
Lambda Layers are a packaging mechanism — reusable code and dependencies that you attach to multiple functions. They're also an underexamined supply chain attack surface. When you attach a public Layer from the AWS Serverless Application Repository or reference someone else's Layer ARN directly, you're executing arbitrary code with your execution role's IAM permissions inside your account's trust boundary.
And teams do this casually. They find a Layer with a useful library, they grab the ARN from a Stack Overflow post or a blog, they attach it. Nobody asks who published it, whether they control that ARN, whether the Layer has been updated since it was last reviewed. A publicly shared Layer can be updated by its publisher at any time — you're referencing an ARN, not pinning a specific version hash.
The AWS SAM and Serverless Framework communities have made Layer sharing almost frictionlessly easy. That frictionlessness is a security debt. Your dependency review process needs to include Lambda Layers. Pin specific Layer versions. Prefer building your own Layers from verified dependencies. If you must use public Layers, treat the review process the same way you'd treat adding a new npm package to a production service — because that's exactly what it is, with the added bonus that it runs in production with full IAM privileges.
Checkov has checks for unversioned Layer references (rule CKV_AWS_45 and related). Run it in CI. Make it a blocking check.
The Framework Gave You Defaults. Those Defaults Are Wrong.
AWS SAM and the Serverless Framework are legitimately useful. They're also responsible for a significant chunk of the misconfigured serverless infrastructure I've reviewed, because their default configurations optimize for getting-something-working rather than getting-something-secure.
Serverless Framework's default IAM role, when you don't specify one, grants the deployment account's permissions to the function. AWS SAM's AutoPublishAlias feature creates IAM policies that are broader than most teams realize. Neither framework defaults to requiring aws:SecureTransport conditions on S3 bucket policies that Lambdas access. API Gateway deployments via both frameworks default to no WAF attachment, no request validation, no throttling.
Read your generated CloudFormation. I know it's verbose and painful. Read it anyway. Understand what permissions your framework is creating on your behalf. Run Checkov against it with the --framework cloudformation flag as a standard part of your deployment pipeline. Treat the framework's output as untrusted generated code that needs review, because that's what it is.
Serverless security isn't fundamentally different from every other application security discipline — it's least privilege, input validation, secrets management, dependency hygiene, and logging done consistently. What's different is that the abstractions actively obscure where those controls need to be applied, and the shared responsibility model leaves more gaps than most teams realize. The cloud provider manages the physical infrastructure and the runtime isolation. Everything above that is yours. And "above that" is where all the interesting attacks live.



