Back to blog
AWSBest PracticesCloud SecurityIdentity & AccessServerless

Lambda Function Allows Public Invocation: Why It's Dangerous and How to Fix It

Learn how to detect and fix AWS Lambda functions with public invoke permissions, scope resource policies correctly, and prevent the misconfiguration with IaC and SCPs.

TL;DR

This check flags Lambda functions whose resource policy lets any AWS account (or anyone at all) invoke them. Scope the policy down to the specific principals or services that need it, and remove any statement with a wildcard Principal or a missing SourceArn condition.

Lambda functions don't run in a vacuum. Something has to invoke them: an API Gateway route, an S3 event, an EventBridge rule, or another service in your account. AWS controls who can do that invoking through a resource-based policy attached to the function. When that policy is too loose, you end up with a function that anyone can trigger, which is exactly what the lambda_public check looks for.

This post walks through what the check detects, why a publicly invokable function is a real problem, and how to lock it down without breaking the integrations that depend on it.


What this check detects

The lambda_public check inspects the resource-based policy on each Lambda function and raises a finding when a statement grants lambda:InvokeFunction (or similar invoke actions) to a principal that is effectively public. In practice that means one of two patterns:

  • A Principal set to "*" or {"AWS": "*"} with no conditions narrowing it down.
  • A service principal (like s3.amazonaws.com or apigateway.amazonaws.com) granted invoke permission without a SourceArn or SourceAccount condition, which leaves it open to confused-deputy abuse from any account using that service.

Here is what a flagged policy looks like:

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "AllowEveryone",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:process-orders"
    }
  ]
}

That single statement means any AWS principal, in any account, can call process-orders.

Note: Lambda has two layers of access control. The execution role governs what the function can do once it runs. The resource policy governs who is allowed to invoke it. This check is about the second layer. A locked-down execution role does nothing to stop an unauthorized invocation.


Why it matters

A function anyone can invoke is an open door, and the damage depends on what the function does. A few realistic scenarios:

Direct cost and abuse

Lambda bills per invocation and per millisecond of compute. An attacker who finds a public function can hammer it in a loop, running up your bill and exhausting concurrency limits that other functions in the account share. If your function calls downstream paid APIs, the cost compounds.

Triggering business logic without authorization

Functions often do real work: sending emails, writing to databases, kicking off payment flows, provisioning resources. If process-orders can be invoked by anyone and it accepts a payload that controls what gets ordered, an attacker can drive that logic directly, bypassing whatever front door you thought protected it.

Reconnaissance and data leakage

Even a function that only returns data is risky if its response leaks internal information. Error messages, stack traces, and verbose logging in a response can hand an attacker a map of your environment.

Warning: The confused-deputy case is easy to overlook. Granting s3.amazonaws.com permission to invoke without a SourceArn means any S3 bucket, including one in an attacker's account, can be configured to trigger your function. The attacker controls the event payload.


How to fix it

The fix is to replace the broad permission with one scoped to the exact principal or service that needs to invoke the function. Lambda resource policies are managed through statement-level permissions, so you remove the bad statement and add a tight one.

Step 1: Inspect the current policy

aws lambda get-policy \
  --function-name process-orders \
  --query Policy --output text | jq .

Note the Sid of each statement. You'll need it to remove the offending one.

Step 2: Remove the public statement

Danger: Removing an invoke permission immediately stops whatever is using it. If a live integration depends on this statement, add the scoped replacement (Step 3) before removing the broad one to avoid breaking production traffic.

aws lambda remove-permission \
  --function-name process-orders \
  --statement-id AllowEveryone

Step 3: Add a scoped permission

For a function fronted by API Gateway, grant invoke only to that API and route:

aws lambda add-permission \
  --function-name process-orders \
  --statement-id apigw-prod-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123/*/POST/orders"

For an S3-triggered function, pin it to the specific bucket and account:

aws lambda add-permission \
  --function-name process-orders \
  --statement-id s3-uploads-invoke \
  --action lambda:InvokeFunction \
  --principal s3.amazonaws.com \
  --source-arn "arn:aws:s3:::my-uploads-bucket" \
  --source-account 123456789012

If another AWS account legitimately needs to invoke the function, name that account explicitly rather than using a wildcard:

aws lambda add-permission \
  --function-name process-orders \
  --statement-id partner-account-invoke \
  --action lambda:InvokeFunction \
  --principal 210987654321

Step 4: Verify

aws lambda get-policy \
  --function-name process-orders \
  --query Policy --output text | jq '.Statement[] | {Sid, Principal, Condition}'

Confirm no statement has "Principal": "*" and that every service principal carries a SourceArn or SourceAccount condition.

Tip: If you're not sure which integrations actually call a function, check CloudTrail for Invoke events and the function's CloudWatch invocation metrics over the last 30 days before you tighten the policy. That tells you who really depends on it.


Fixing it in infrastructure as code

If the function is managed by Terraform or CloudFormation, fix it there so a future apply doesn't reintroduce the problem.

Terraform:

resource "aws_lambda_permission" "allow_apigw" {
  statement_id  = "apigw-prod-invoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.process_orders.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.orders.execution_arn}/*/POST/orders"
}

CloudFormation:

AllowApiGwInvoke:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !Ref ProcessOrdersFunction
    Action: lambda:InvokeFunction
    Principal: apigateway.amazonaws.com
    SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${OrdersApi}/*/POST/orders"

How to prevent it from happening again

Manual remediation fixes today's finding. To stop the pattern from coming back, push the check left into your pipeline and your guardrails.

Scan IaC before it merges

Tools like Checkov or tfsec catch wildcard Lambda principals during a pull request. A minimal Checkov gate in CI:

checkov -d . --framework terraform \
  --check CKV_AWS_61,CKV_AWS_364

Fail the build on findings so an overly permissive policy never reaches an apply.

Block it organization-wide with an SCP

A Service Control Policy can deny the creation of any permission that uses a public principal, stopping the misconfiguration before it exists. Pair this with detective monitoring so you catch anything created out of band.

Tip: Run lambda_public on a schedule across every account rather than once. Resource policies change as teams wire up new triggers, and a function that was tight last month can drift open the moment someone runs add-permission with a wildcard to "just get it working."


Best practices

  • Always scope service principals. Whenever you grant lambda:InvokeFunction to an AWS service, include SourceArn and, where supported, SourceAccount. This is your defense against confused-deputy attacks.
  • Never use Principal: "*" on an invoke permission. If you genuinely need cross-account access, name the account IDs. There is no legitimate reason for a Lambda function to be invokable by the entire internet.
  • Prefer Function URLs with auth over public resource policies. If you need HTTP access to a function, a Lambda Function URL with AuthType: AWS_IAM gives you a controlled entry point instead of a wide-open invoke permission.
  • Keep payloads untrusted. Validate and sanitize every event your function receives. Assume the invoker is hostile and the payload is attacker-controlled, even behind a scoped policy.
  • Set reserved concurrency on sensitive functions. It won't stop unauthorized invocation, but it caps the blast radius of an invocation flood and protects the rest of your account's concurrency pool.
  • Review resource policies during every architecture change. New triggers mean new statements. Make policy review part of the same PR that adds the trigger.

A Lambda function should only ever be invokable by the things that have a reason to invoke it. Tighten the policy to that exact set, enforce it in code, and keep scanning, so a quick "make it work" change doesn't quietly turn into a public endpoint.