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

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:

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:

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

Serverless Security Testing Checklist

Event injection

IAM and permissions

Secrets and configuration

Network and SSRF

API and function URLs

Dependencies

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
← Back to Blog