Serverless platforms like AWS Lambda, Azure Functions, and Google Cloud Functions abstract the infrastructure entirely — but they do not abstract the security. This article examines the unique attack surface, isolation model, and security controls for serverless workloads.
Sign in to mark this article as read and track your progress.
Serverless platforms are marketed as reducing operational burden. This is true — patching OS and runtime is the provider's responsibility. However, serverless concentrates security responsibility at layers that are harder to protect:
A serverless application with a SQL injection vulnerability, an over-privileged execution role, and secrets in environment variables is more dangerous than an equivalent application on a hardened EC2 instance — because there is no network-level control, no OS-level detection, and no persistent file system to examine for forensics.
AWS Lambda executes each function invocation in a micro-VM based on Firecracker (AWS's open-source virtual machine monitor). Firecracker provides:
However, within a single Lambda invocation, the execution environment is shared with subsequent invocations of the same function (execution environment reuse). This means:
/tmp persist between invocations (intended design for caching)| Layer | Attack | Prevention |
|---|---|---|
| Trigger | Public URL trigger with no authentication | Require API Gateway authoriser (JWT/IAM); no unauthenticated triggers |
| Code | Injection via event input (os.system(event['cmd'])) | Input validation; never pass user input to shell commands |
| Dependencies | Malicious npm/pip package with the function code | Scan with Snyk/Trivy before deployment; SBOM generation |
| IAM Role | Over-privileged execution role | Least privilege; use resource-level conditions |
| Environment variables | Secrets in env vars readable by anyone with access to Lambda config | Use Secrets Manager; never put secrets in env vars |
| Execution environment | /tmp left with sensitive data from previous invocation | Clear /tmp at function start if sensitive data used |
| VPC | Lambda in VPC cannot reach internet without NAT Gateway; NAT Gateway misconfigured to allow inbound | Review NAT Gateway security; use VPC endpoint for AWS services |
A "cold start" occurs when Lambda must initialise a new execution environment (micro-VM boot + runtime initialisation + package import). A "warm start" reuses an existing execution environment.
Security implications of warm execution:
import boto3
# Module-level (global) — runs only on cold start
s3_client = boto3.client('s3')
db_connection = connect_to_database() # Risky if connection carries state
def handler(event, context):
# Handler code — runs on every invocation (warm or cold)
user_id = event['user_id']
# If db_connection is reused from previous invocation, ensure
# no residual state from previous user persists
result = db_connection.query(f"SELECT * FROM orders WHERE user_id = {user_id}")
# ↑ SQL injection vulnerability AND connection state risk
Serverless functions consume events from many sources. Each event source is an injection vector if input is not validated:
| Event Source | Injection Risk | Example |
|---|---|---|
| API Gateway | HTTP injection, SSRF | event['body'] contains curl http://169.254.169.254/ |
| SQS | Message body injection | Worker processes unvalidated SQS message as shell command |
| S3 Event | Path traversal in object key | Object key ../../etc/passwd parsed as file path |
| DynamoDB Stream | NoSQL injection in record data | Unvalidated DynamoDB attribute used in AWS SDK call |
| SNS | Message content injection | SNS message triggers Lambda that emails the content raw |
Principle: Treat every Lambda event as untrusted input, regardless of source. AWS SQS messages can be injected by any AWS principal with sqs:SendMessage on the queue — validate the schema.
| Control | Implementation |
|---|---|
| One function, one IAM role | No shared execution roles across functions with different trust levels |
| Resource-level IAM conditions | s3:GetObject on arn:aws:s3:::my-bucket/path/${aws:userid}/* only |
| No secrets in environment variables | Load from Secrets Manager at cold start (cache in memory, refresh on TTL) |
| Enable X-Ray tracing | Distributed trace visible in X-Ray; performance + security observability |
| Reserved concurrency | Limit blast radius: prevent one function from consuming all account concurrency |
| Dead Letter Queue (DLQ) | Failed invocations land in SQS/SNS for analysis; don't lose events silently |
| Function URL authentication (if used) | Require AWS_IAM auth type; never use NONE for sensitive functions |
| Layer version pinning | Do not use :latest layer ARNs in production; pin to specific version |