Cloud Security Wire
AWS Azure GCP RSS
AWS Hardening Guide high

AWS Lambda Security: Malicious Layers, IAM Overreach, and Hardening the Serverless Attack Surface

Serverless functions expand your attack surface in ways traditional scanner tools miss. Lambda's IAM roles, event source inputs, Layers, and runtime environment all present exploitation opportunities that compound quickly when misconfigured.

By Cloud Security Wire · ·
#AWS#Lambda#serverless#IAM#malicious-layers#SSRF#environment-variables#event-injection#cloud-security
High Severity

This issue has been assessed as high severity. Review affected configurations immediately.

Serverless architectures are attractive because they abstract away infrastructure management. They are also attractive to attackers for the same reason: less visibility, more implicit trust, and a blast radius that extends well beyond the function itself.

Lambda security issues cluster around four areas: IAM roles that grant far more than the function needs, event source inputs that are insufficiently validated, Lambda Layers that can be poisoned if an account is compromised, and environment variables that leak credentials into logs or error messages. Here is how each works and how to lock them down.

Overpermissive IAM Execution Roles

The most common Lambda misconfiguration, by volume, is an execution role with permissions the function never actually needs. A function that reads from one S3 bucket may have an execution role with s3:* or — worse — * on *.

When a Lambda function is compromised via RCE (through a dependency vulnerability, deserialization flaw, or code injection in event input), the attacker inherits that execution role. Every IAM permission the role holds is available to them immediately.

Check your Lambda functions’ effective permissions:

# List all Lambda functions
aws lambda list-functions --query 'Functions[*].[FunctionName,Role]' --output table

# Get the role's attached policies for a specific function
ROLE_NAME="my-lambda-execution-role"
aws iam list-attached-role-policies --role-name "$ROLE_NAME"
aws iam list-role-policies --role-name "$ROLE_NAME"

# Simulate what the role can actually do (run from your workstation)
aws iam simulate-principal-policy \
  --policy-source-arn "arn:aws:iam::ACCOUNT_ID:role/$ROLE_NAME" \
  --action-names "s3:GetObject" "sts:AssumeRole" "secretsmanager:GetSecretValue" \
  --resource-arns "*"

Minimal example IAM execution role policy (function reads one S3 bucket, writes to CloudWatch Logs):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:eu-west-1:123456789012:log-group:/aws/lambda/my-function:*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-specific-bucket/*"
    }
  ]
}

Never use sts:AssumeRole broadly in a Lambda execution role unless the function genuinely needs to cross-account. It is a privilege escalation path: an attacker who compromises the function can assume other roles in the same or other accounts.

Malicious Lambda Layers

Lambda Layers allow you to share code, dependencies, and configuration across functions. They are also a persistence mechanism for attackers who gain AWS account access.

If an attacker gets temporary credentials (via SSRF, stolen environment variables, or a misconfigured CI/CD pipeline), they can:

  1. Create or modify a Lambda Layer containing a malicious payload
  2. Attach that Layer to any Lambda function in the account
  3. The payload executes in the function’s runtime context — including the function’s IAM execution role

The critical detail: Layers persist even if the function code is redeployed. Incident response teams that re-deploy function code but miss an attached malicious Layer are leaving the backdoor open.

Audit Lambda Layers across your account:

# List all Lambda Layer versions in your account
aws lambda list-layers --query 'Layers[*].[LayerName,LatestMatchingVersion.LayerVersionArn]' --output table

# For each function, check which Layers are attached
aws lambda list-functions --query 'Functions[*].[FunctionName]' --output text | \
while read FUNC; do
  echo "=== $FUNC ==="
  aws lambda get-function-configuration --function-name "$FUNC" \
    --query 'Layers[*].Arn' --output text
done

# Check if a Layer has public access (resource-based policy)
aws lambda get-layer-version-policy \
  --layer-name my-layer \
  --version-number 1 2>/dev/null || echo "No public policy"

Layers with public access (Principal: *) are accessible to anyone with an AWS account. Audit all public Layers and remove public access unless intentionally published.

Event Source Injection

Lambda functions are triggered by event sources — API Gateway, S3, SQS, SNS, DynamoDB Streams, EventBridge. Each event source delivers a JSON payload to the handler. If the function passes event data into shell commands, SQL queries, template engines, or other interpreters without validation, it becomes an injection target.

Common vulnerable pattern (Node.js) — event data into shell:

// VULNERABLE: never do this
exports.handler = async (event) => {
  const filename = event.queryStringParameters.filename;
  const result = await exec(`cat /tmp/${filename}`);  // command injection
  return { statusCode: 200, body: result.stdout };
};

Safe alternative:

const path = require('path');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');

exports.handler = async (event) => {
  const filename = event.queryStringParameters.filename;
  
  // Validate against allowlist
  if (!/^[a-zA-Z0-9_-]{1,64}\.txt$/.test(filename)) {
    return { statusCode: 400, body: 'Invalid filename' };
  }
  
  // Use AWS SDK instead of shell  --  no injection surface
  const client = new S3Client({});
  const response = await client.send(new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME,
    Key: `uploads/${filename}`,
  }));
  
  return { statusCode: 200, body: await response.Body.transformToString() };
};

For functions exposed via API Gateway, treat the entire event body as untrusted external input regardless of any upstream validation. API Gateway request validation helps but does not eliminate injection risk.

SSRF to the Instance Metadata Service

Lambda functions run with access to the instance metadata service at 169.254.169.254. A function vulnerable to SSRF can be used to steal its execution role credentials, which can then be used outside the Lambda context.

The credentials available at http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> are temporary STS tokens, but they are valid and fully functional AWS credentials until they expire (up to 12 hours).

Mitigation: IMDSv2 requires a PUT request to get a session token before metadata requests. For Lambda functions, this is less directly applicable than for EC2, but you should:

  1. Ensure any HTTP client library in your Lambda function cannot make requests to 169.254.0.0/16 range. This is architecture-dependent — consider blocking at network policy level using VPC security groups if the function runs in a VPC.
  2. Scope execution roles tightly (see above) so that even if credentials are stolen, the blast radius is limited.

Environment Variable Credential Management

Lambda environment variables are a common place to store credentials, connection strings, and API keys. They are not secrets by default — they appear in plaintext in the Lambda configuration, CloudTrail logs, and can be read by anyone with lambda:GetFunctionConfiguration.

Never store secrets as plaintext environment variables. Instead:

# Store a secret in AWS Secrets Manager
aws secretsmanager create-secret \
  --name "prod/my-function/db-credentials" \
  --secret-string '{"username":"app","password":"CHANGEME"}'

# Grant the Lambda execution role access to this specific secret
aws iam put-role-policy \
  --role-name my-lambda-execution-role \
  --policy-name SecretsManagerAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:iam::123456789012:secret:prod/my-function/db-credentials-*"
    }]
  }'

In the Lambda function, retrieve secrets at cold start and cache them for the function lifetime. This minimises API calls while keeping secrets out of the configuration.

What to Audit Now

Four checks that catch the most common Lambda security issues:

  1. Wildcard IAM actions: aws iam simulate-principal-policy or AWS IAM Access Analyzer to flag execution roles with * permissions.
  2. Attached Layers: Audit every function’s Layer list; investigate any Layer not from your team’s source control.
  3. Public Layer policies: Check for Principal: * on any Layer version you own.
  4. Environment variable secrets: Search function configurations for credentials stored as plain environment variable strings rather than Secrets Manager ARN references.

None of these require additional tooling beyond the AWS CLI. The findings are almost always more significant than teams expect.

← All Analysis Subscribe via RSS