Serverless Security Testing: AWS Lambda, Azure Functions, and GCP Cloud Run
Serverless architectures eliminate servers but do not eliminate attack surface. They shift the risk: less OS-level hardening to worry about, more IAM misconfiguration, event injection, and supply chain exposure to deal with. The security perimeter moved, and most teams have not adjusted their testing methodology to match.
This guide covers the serverless-specific attack surface for AWS Lambda, Azure Functions, and GCP Cloud Functions/Cloud Run, and what a practical security assessment of serverless infrastructure looks like.
The Serverless Security Model
In a serverless architecture, the cloud provider owns the runtime, OS, and hypervisor. The developer owns the function code, the event sources that trigger it, the IAM permissions granted to it, and the environment variables it runs with. Security responsibilities shift accordingly:
| Layer | Who owns it | Security concern |
|---|---|---|
| Infrastructure / runtime | Cloud provider | Shared responsibility — provider patches the runtime |
| Function code | Developer | Injection, insecure dependencies, business logic flaws |
| Event sources | Developer | Unvalidated event data, spoofed event sources |
| IAM / permissions | Developer | Over-permissioned execution roles, privilege escalation |
| Environment variables | Developer | Secrets in env vars, exposed in logs or error responses |
| Third-party dependencies | Developer | Vulnerable npm/PyPI/Maven packages included in deployment |
| Network access | Developer | SSRF via function outbound calls, VPC configuration |
Event Injection
Serverless functions are triggered by events from many sources: API Gateway HTTP requests, S3 object uploads, SQS messages, SNS notifications, DynamoDB streams, EventBridge rules, Kafka topics. Each event source is a potential injection vector.
API Gateway as event source
When Lambda sits behind API Gateway, the HTTP request becomes a structured event object. The function code must extract and validate parameters carefully:
import json
def handler(event, context):
# Event from API Gateway
user_id = event['pathParameters']['userId'] # Path parameter
query = event['queryStringParameters']['q'] # Query string
body = json.loads(event['body']) # Request body
# If user_id goes directly into a database query without validation...
result = db.query(f"SELECT * FROM users WHERE id = '{user_id}'")
The injection surface is identical to a traditional web application — SQL injection, command injection, SSRF — but the entry point is the event object rather than an HTTP request parsed by a framework. The difference: many Lambda functions lack the input validation middleware that web frameworks provide by default.
S3 event injection
Functions triggered by S3 uploads process the object key and content as trusted input. If the function uses the object key in a downstream operation:
def handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# Path traversal if key is not validated
local_path = f"/tmp/{key}"
os.system(f"convert {local_path} output.png") # Command injection via key
An attacker who can trigger uploads (or who controls the naming of uploaded files) can inject malicious key names: ../../etc/passwd for path traversal, or shell metacharacters for command injection.
SQS and SNS message injection
Messages from queues and topics are often treated as trusted internal events. If the message originates from user-controlled input upstream in the pipeline, every consumer function in the chain inherits the injection risk. Test what happens when message bodies contain: SQL metacharacters, JSON with unexpected fields, XML with external entity references, and oversized payloads.
IAM and Privilege Escalation
IAM misconfiguration is the most common high-severity finding in serverless environments. The Lambda execution role defines what the function can do — and over-permissioned roles are everywhere.
Common over-permissions
s3:*on all buckets when the function only needs to read one bucketdynamodb:*when read-only access is sufficientiam:PassRolewhich allows the function to assign roles to other AWS services — often a privilege escalation pathlambda:InvokeFunctionon all functions in the accountec2:*when the function only uses EC2 metadata for VPC configuration
IAM privilege escalation via Lambda
If an attacker achieves code execution in a Lambda function (via injection), the function's execution role becomes their foothold. Common escalation paths:
import boto3
# From inside a compromised Lambda function
sts = boto3.client('sts')
identity = sts.get_caller_identity()
print(identity) # Shows current role ARN
# If the role has iam:ListAttachedRolePolicies
iam = boto3.client('iam')
role_name = identity['Arn'].split('/')[-1]
policies = iam.list_attached_role_policies(RoleName=role_name)
# If iam:CreatePolicyVersion or iam:AttachRolePolicy is available
# — attacker can escalate to full admin
Testing: after getting code execution (via injection or by testing with your own function), enumerate what the execution role can do. Use tools like Pacu (AWS exploitation framework) or enumerate-iam to map available permissions.
IMDSv1 and the metadata service
Lambda functions running in a VPC with EC2 instances may have access to the EC2 Instance Metadata Service (IMDS). If an SSRF vulnerability exists in the function, an attacker can use it to query the metadata service and steal IAM credentials:
# SSRF payload targeting IMDSv1 (no auth required)
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns the role name, then:
http://169.254.169.254/latest/meta-data/iam/security-credentials/LambdaExecutionRole
IMDSv2 (the default since 2019 for EC2, but configurable) requires a PUT request to get a token first, which SSRF cannot do from a GET-only context. Always enforce IMDSv2 on all EC2 instances in VPCs that contain Lambda functions.
Secrets in Environment Variables
Environment variables are the most common way to pass secrets to Lambda functions — database credentials, API keys, JWT secrets. They are visible in:
- The Lambda function configuration in the AWS Console
- CloudFormation stacks, SAM templates, and Terraform state files stored in S3
- Application error messages that dump the execution context
- CloudWatch Logs if the application logs the environment
Check for unencrypted environment variables containing secrets:
# List all Lambda functions
aws lambda list-functions --query 'Functions[*].FunctionName'
# Get environment variables for a function
aws lambda get-function-configuration --function-name my-function \
--query 'Environment.Variables'
# Check if KMS encryption is configured
aws lambda get-function-configuration --function-name my-function \
--query 'KMSKeyArn'
The fix: use AWS Secrets Manager or Parameter Store with encryption. Fetch secrets at runtime rather than injecting them as environment variables. At minimum, enable KMS encryption for environment variables.
Cold Start Timing Attacks
Lambda cold starts create timing differences between first-invocation (cold) and warm invocations. This can leak information in authentication flows: if a cold start occurs for non-existent users (because the lookup exits early) but not for existing users (which warm the function), response timing reveals valid usernames.
Test authentication endpoints for timing side channels:
import time
import requests
# Test with a known-valid username
times_valid = []
for i in range(10):
start = time.time()
requests.post('/auth/login', json={'username': 'valid@example.com', 'password': 'wrong'})
times_valid.append(time.time() - start)
# Test with a non-existent username
times_invalid = []
for i in range(10):
start = time.time()
requests.post('/auth/login', json={'username': 'noexist@example.com', 'password': 'wrong'})
times_invalid.append(time.time() - start)
Consistent timing differences of more than ~10ms between valid and invalid usernames indicate a timing oracle.
Dependency and Supply Chain Risk
Lambda deployment packages bundle all dependencies. The attack surface scales with the number of packages:
- Node.js functions with hundreds of transitive npm dependencies
- Python functions with PyPI packages that have known CVEs
- Lambda Layers shared across functions — a vulnerable layer affects all consumers
Scan deployment packages for vulnerable dependencies:
# Node.js
npm audit --audit-level=high
# Python
pip-audit -r requirements.txt
# Or scan the packaged zip
unzip function.zip -d /tmp/function
cd /tmp/function
pip-audit --requirement requirements.txt
Function URL and API Gateway Security
Unauthenticated function URLs
AWS Lambda Function URLs can be configured with AuthType: NONE, making them publicly accessible without any IAM authentication. Enumerate function URLs in your account:
aws lambda list-function-url-configs --function-name my-function
Verify that every public-facing function URL either uses AuthType: AWS_IAM or has its own application-layer authentication.
API Gateway authorization gaps
- Missing authorizer — API Gateway route with no Lambda authorizer, Cognito, or IAM auth
- Authorizer misconfiguration — authorizer caches authorization decisions; test whether a valid token for user A can authorize user A's requests to user B's resources if the cache key does not include the resource identifier
- HTTP method mismatch —
GET /resourceis authenticated butOPTIONS /resource(CORS preflight) is not — check CORS responses for sensitive headers
Serverless Security Testing Checklist
Event injection
- Identify all event sources for each function (API Gateway, S3, SQS, SNS, DynamoDB, EventBridge)
- Test all string fields in event data for SQL injection, command injection, path traversal
- Test S3 key handling for path traversal and shell metacharacters
- Test oversized payloads and unexpected field types
IAM and permissions
- Enumerate execution role permissions for all functions
- Identify wildcard permissions (
s3:*,dynamodb:*,iam:*) - Check for
iam:PassRole,iam:CreatePolicyVersion,lambda:AddPermission— privilege escalation paths - Verify IAM roles follow least-privilege principle
Secrets and configuration
- Check environment variables for plaintext secrets
- Verify KMS encryption is enabled for environment variables
- Check CloudFormation/Terraform state for exposed secrets
- Verify CloudWatch Logs do not contain secrets from error dumps
Network and SSRF
- Test all URL-accepting inputs for SSRF targeting 169.254.169.254
- Verify IMDSv2 is enforced on EC2 instances in the same VPC
- Review VPC configuration — functions should not have more network access than required
API and function URLs
- Enumerate all function URLs and verify authentication type
- Test API Gateway routes for missing authorizers
- Test authorizer caching for cross-user authorization bypass
- Test CORS configuration on API Gateway endpoints
Dependencies
- Scan all deployment packages for vulnerable dependencies
- Audit Lambda Layers for outdated packages
- Verify package integrity in CI/CD pipeline (lockfiles, hash verification)
Tools Reference
| Tool | Use case |
|---|---|
| Pacu | AWS exploitation framework — IAM enumeration, privilege escalation |
| enumerate-iam | Enumerate available IAM permissions from a set of credentials |
| Prowler | AWS security assessment — checks Lambda configuration, IAM, CloudTrail |
| ScoutSuite | Multi-cloud security audit (AWS, Azure, GCP) including serverless |
| Checkov | IaC scanning — catches over-permissioned Lambda roles in Terraform/CloudFormation |
| pip-audit / npm audit | Dependency vulnerability scanning for Python and Node.js packages |
| truffleHog / gitleaks | Secret scanning in IaC files and deployment artifacts |
| Burp Suite | HTTP-level testing for API Gateway endpoints (injection, auth bypass) |
Ironimo scans the API surface of your serverless application — API Gateway endpoints, function URLs, and the web application layer in front of Lambda functions. Catch injection vulnerabilities, authentication gaps, and SSRF before attackers use them to compromise your Lambda execution role.
Start free scan