8 min read

Serverless Security Testing

Serverless functions (AWS Lambda, Azure Functions, Google Cloud Functions) introduce unique attack surfaces: event injection, over-permissive IAM roles, function chaining abuse, and cold-start behaviour. Traditional network-based pentest tools often don't apply — serverless requires a different approach.

Frameworks: WSTG n/a (closest: WSTG-CONF-05, -11) · ASVS 5.0 V15.2.3 (also V13.2.4) · CWE-269, CWE-918, CWE-798 · T1078.004, T1552.005

What Serverless Changes

The handler is still an application: it parses untrusted input and talks to a database. What changes is the perimeter — input arrives as a structured event another team defines, the runtime injects temporary credentials, the environment is reused between invocations, and the blast radius is whatever the execution role can reach. Injection needs an invocation path and a handler that passes event fields into something that interprets them; role and /tmp testing need a foothold inside the function. This page introduces those mechanics.

Event Injection

The event is the request. API Gateway's proxy integration gives the handler an object whose body is a string, so the application parses a second time — a payload rejected in one parse may be accepted in the other. SQS and SNS are more direct: whoever can write to the queue controls the body verbatim. A positive result is the payload's effect downstream: a row, a file, a 500 whose stack trace contains the query. A validation error raised before the field is used is a benign result.

serverless-event-injection.sh
# Serverless functions are triggered by events. Inject into event sources:

# API Gateway → Lambda (HTTP event injection):
curl -X POST https://API_ID.execute-api.us-east-1.amazonaws.com/prod/user \
  -H 'Content-Type: application/json' \
  -d '{"username": "admin\' OR 1=1 --"}'

# S3 event injection:
# If a Lambda processes uploaded files, upload malicious files:
# - ZIP bomb (denial of service)
# - XML file triggering XXE in the parser
# - Image with embedded code (ImageMagick exploitation)

# SQS/SNS event injection:
# If the function processes messages from a queue:
# Inject malicious payloads into the queue message body

# DynamoDB Stream injection:
# If a function triggers on database changes:
# Insert data that causes injection when the function processes it

# Cognito trigger injection:
# Pre-signup, post-auth triggers may process user-controlled data
# Register with malicious values in custom attributes

Dependency and Layer Trust

Layers are the part of the supply chain teams forget. A layer is a versioned archive mounted at /opt and shared by every function that references it, so one update changes all of them — and pipelines scan the function package while ignoring the layer. Pinning a version ARN makes the dependency set reproducible. Read the layer version from the config, compare the artifact against the published package, and check whether the function resolves the module from /opt/nodejs/node_modules before its own bundle: resolution order decides which copy loads.

Environment & Configuration Exposure

The runtime injects AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN for the execution role. Encryption at rest does not help against a principal holding lambda:GetFunctionConfiguration, which returns them in clear. A positive finding is a live secret that also works from your machine; a benign one is a placeholder or a key scoped to a resource you already control.

serverless-env-exposure.sh
# Lambda environment variables often contain secrets:
# If you achieve code execution in a Lambda:
env | grep -iE 'key|secret|token|password|database|connection'

# AWS Lambda has these environment variables by default:
# AWS_ACCESS_KEY_ID
# AWS_SECRET_ACCESS_KEY
# AWS_SESSION_TOKEN
# AWS_LAMBDA_FUNCTION_NAME
# AWS_REGION

# These temporary credentials can be used for AWS API calls:
aws sts get-caller-identity
aws s3 ls
aws iam list-roles

# Check /tmp for data from previous invocations (shared container):
ls -la /tmp/
cat /tmp/*

# Read the function code:
ls -la /var/task/
cat /var/task/index.js
cat /var/task/handler.py

# Check IAM permissions (what can this function access?):
# Derive the execution role ARN (sts returns an assumed-role ARN):
ROLE_ARN=$(aws sts get-caller-identity --query Arn --output text \
  | sed -E 's#arn:aws:sts::([0-9]+):assumed-role/([^/]+)/.*#arn:aws:iam::\1:role/\2#')
# Requires iam:SimulatePrincipalPolicy:
aws iam simulate-principal-policy \
  --policy-source-arn "$ROLE_ARN" \
  --action-names 's3:GetObject' 's3:PutObject' 'dynamodb:Scan'

/tmp and Execution-Environment Reuse

/tmp is ephemeral storage — 512 MB by default, up to 10 GB — that survives while the environment stays warm, and one environment serves many invocations. That makes it a cross-invocation leak surface rather than a filesystem: a cache written for one request can be served to the next. Prove it with two invocations and a marker.

serverless-tmp-reuse.sh
# Invocation 1 — drop a marker in ephemeral storage
echo "probe-$(date +%s)" > /tmp/hm-probe

# Invocation 2 — same function, hopefully the same warm environment
cat /tmp/hm-probe 2>/dev/null && echo REUSED || echo FRESH

Privilege Escalation via Over-Permissive Roles

The execution role is the real blast radius. iam:SimulatePrincipalPolicy answers what the role can do without making the calls; the escalation worth hunting is a role that can invoke or rewrite a more privileged function (lambda:InvokeFunction, lambda:UpdateFunctionCode) or pass a role (iam:PassRole). A positive result is code running as a broader identity than the one you started in.

serverless-iam-escalation.sh
# Common issue: Lambda functions with wildcard IAM permissions
# If the function has s3:* or dynamodb:*, exploit it:

# Read any S3 bucket:
aws s3 ls s3://company-secrets-bucket/
aws s3 cp s3://company-secrets-bucket/credentials.json ./

# Scan DynamoDB tables:
aws dynamodb list-tables
aws dynamodb scan --table-name Users

# Invoke other Lambda functions:
aws lambda list-functions
aws lambda invoke --function-name admin-function output.json
cat output.json

# Modify other functions (if lambda:UpdateFunctionCode is allowed):
# This is a critical escalation — you can inject code into other functions

# Access Secrets Manager:
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id prod/database/credentials

Cold Starts and Concurrency Abuse

Cold starts are over-sold. A cold start is the platform initialising a new environment: init code runs again and latency jumps by hundreds of milliseconds. That is a fingerprinting and availability surface, not a dependable data side channel — concurrency and payload size swamp any signal you could attribute to another tenant. What is real: reserved concurrency is a hard cap, so exhausting it throttles every invocation (a denial of service with an invoice attached), and init-phase code runs once per environment, a good place to look for baked-in secrets.

Testing Checklist

  • ☐ 1. Identify serverless endpoints (API Gateway URLs, function URLs)
  • ☐ 2. Test injection in all event sources (HTTP, queue messages, file uploads)
  • ☐ 3. Test for insecure deserialization of event payloads
  • ☐ 4. Check for leaked environment variables in error messages
  • ☐ 5. Test function timeout and memory limits (resource exhaustion DoS)
  • ☐ 6. Assess IAM role permissions (are they over-permissive?)
  • ☐ 7. Test for data persistence in /tmp between invocations
  • ☐ 8. Check if functions are publicly invocable without auth
  • ☐ 9. Inventory layers and their versions; test whether the function resolves a library from /opt before its own bundle
  • ☐ 10. Test warm-environment reuse: write a marker to /tmp, invoke again, read it back

Evidence Collection

Function Mapping: List of discovered functions, event sources, and permissions

Injection: Request/response showing successful event injection

IAM Assessment: Over-permissive policies with wildcard actions

Layer Inventory: Layer name, version and ARN per function, plus the resolved module path that proves which copy loaded

Warm Reuse: Two invocations: the marker written by the first, and the same marker read by the second

CVSS Range: Event injection: 7.5–9.1 | Credential exposure: 8.6–9.8 | IAM escalation: 9.1–9.8

Remediation

  • • Least privilege IAM: Grant only the specific permissions each function needs — never use * wildcards.
  • • Validate all events: Never trust event data — validate and sanitize all fields from event sources.
  • • Use Secrets Manager: Never store secrets in environment variables — use AWS Secrets Manager or similar.
  • • Set resource limits: Configure timeout, memory, and concurrency limits to prevent abuse.
  • • Restrict function access: Use API Gateway authorizers, require authentication for all function URLs.
  • • Pin layers: Reference a specific layer version ARN in every function, scan the layer artifact like application code, and treat a layer update as a production change.
  • • Bound concurrency: Set reserved concurrency per function and alarm on throttle counts so exhausting one function cannot starve the shared account pool.

False Positive Identification

  • Env vars in logs by design: Some logging frameworks intentionally log environment context — verify the logged variables contain actual secrets, not just config metadata.
  • Broad IAM role by design: Some architectures use shared roles across functions intentionally — report the risk but assess if the role follows least-privilege for the function group.
  • Cold start timing variance: Variable response times in serverless don't indicate a timing attack — cold starts naturally cause 100-500ms variance.
  • Throttling during load: 429s under a burst are the concurrency limit working as designed. A finding needs the throttle to persist at low request rates, or to be reachable by a single unauthenticated caller.
  • A marker that survived by luck: <code>/tmp</code> reuse is not guaranteed — the platform may recycle the environment between your invocations. Repeat the test several times before reporting persistence.

Serverless Practice

Attack function event data, IAM scope, and dependency handling. Both labs are available, though both have low maintenance velocity (neither is archived).

OWASP ServerlessGoat Custom Lab medium
Event data injectionOver-permissive IAMDependency abuse
OWASP DVSA Custom Lab hard
Function event abuseSecrets in environmentPrivilege escalation