When a Lambda function's resource policy grants invoke permission to an AWS service like S3 or SNS but omits a SourceArn condition, any resource of that type, in any account, can trigger your function. Add a SourceArn (and ideally SourceAccount) condition to scope the permission to the exact resource you intend.
Lambda functions rarely run in isolation. They get triggered by S3 object uploads, SNS topic notifications, EventBridge rules, API Gateway requests, and dozens of other AWS services. To make that work, you grant those services permission to invoke your function through a resource-based policy attached to the function itself.
The problem starts when that permission is too broad. If you tell Lambda "allow S3 to invoke this function" without specifying which S3 bucket, you have effectively told Lambda that any S3 bucket can invoke it. That is the gap this check flags, and it is a textbook confused deputy setup.
What this check detects
The lambda_nosourcearn check inspects every statement in a Lambda function's resource-based policy. It looks for statements that:
- Grant the
lambda:InvokeFunctionaction - Set the principal to an AWS service (for example
s3.amazonaws.com,sns.amazonaws.com, orevents.amazonaws.com) - Do not include a
SourceArncondition key to restrict which specific resource can invoke the function
When a statement matches all three, the check fails. The function is reachable by any resource of that service type, not just the one you wired it up to.
Note: Lambda resource policies are separate from IAM execution roles. The execution role controls what the function can do once it runs. The resource policy controls who is allowed to invoke it. This check is about the second one.
Why it matters
The confused deputy problem
A confused deputy is a privileged component that gets tricked into using its authority on behalf of an attacker. In this case, the "deputy" is the AWS service principal. When you grant s3.amazonaws.com permission to invoke your function with no SourceArn, you are trusting the S3 service to act correctly, but you are not constraining which bucket's events it acts on.
Consider a concrete scenario. Your function processes uploads from my-app-prod-uploads. You add the permission but forget the SourceArn. Now an attacker who controls any S3 bucket in any AWS account can configure that bucket to send event notifications to your function. They craft a malicious payload, your function processes it as if it were a legitimate upload, and depending on what the function does next, they may be able to poison downstream data, trigger expensive operations, or escalate into other parts of your environment.
Warning: The attacker does not need access to your account to exploit this. With service principals like S3 and SNS, the invocation comes through the AWS service itself, so a missing SourceArn opens the door to cross-account abuse from resources you have never heard of.
Real business impact
- Unexpected cost: A function invoked by attacker-controlled events runs on your bill. High-volume triggering can rack up Lambda and downstream charges fast.
- Data integrity: Functions that write to databases or queues can be fed forged input, corrupting state.
- Lateral movement: If the function assumes a role with broad permissions, attacker-supplied input can become the entry point into a larger blast radius.
How to fix it
The fix is to add a SourceArn condition that pins the permission to the exact resource you want. For services that are account-scoped rather than resource-scoped, add a SourceAccount condition as well.
Step 1: Inspect the current policy
Pull the existing resource policy so you can see what is granted and to whom.
aws lambda get-policy \
--function-name my-upload-processor \
--query 'Policy' \
--output text | jq .
Look for any Statement where Principal.Service is set and there is no Condition.ArnLike.AWS:SourceArn entry.
Step 2: Remove the overly broad permission
You cannot edit a statement in place, so remove it by its statement ID and re-add it with the condition.
Danger: Removing a permission statement immediately stops the associated trigger from invoking your function until you re-add a scoped version. Do this during a maintenance window or in quick succession to avoid dropping events in production.
aws lambda remove-permission \
--function-name my-upload-processor \
--statement-id s3invoke
Step 3: Re-add the permission with a SourceArn
For an S3 trigger, include both --source-arn (the bucket ARN) and --source-account (the bucket owner's account ID). S3 bucket names are global, so SourceAccount closes a gap where a bucket could be recreated in another account.
aws lambda add-permission \
--function-name my-upload-processor \
--statement-id s3invoke \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::my-app-prod-uploads \
--source-account 111122223333
For an SNS topic:
aws lambda add-permission \
--function-name my-notify-handler \
--statement-id snsinvoke \
--action lambda:InvokeFunction \
--principal sns.amazonaws.com \
--source-arn arn:aws:sns:us-east-1:111122223333:order-events
For an EventBridge rule:
aws lambda add-permission \
--function-name my-cron-job \
--statement-id eventbridgeinvoke \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:111122223333:rule/nightly-rollup
Step 4: Fix it in infrastructure as code
If you manage Lambda with Terraform, the aws_lambda_permission resource has a source_arn argument. Set it.
resource "aws_lambda_permission" "allow_s3" {
statement_id = "s3invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.upload_processor.function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.uploads.arn
source_account = data.aws_caller_identity.current.account_id
}
In AWS SAM or CloudFormation, set SourceArn on the permission:
{
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": { "Ref": "UploadProcessor" },
"Action": "lambda:InvokeFunction",
"Principal": "s3.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["UploadsBucket", "Arn"] },
"SourceAccount": { "Ref": "AWS::AccountId" }
}
}
Tip: If you define the event source mapping in SAM (for example an S3 or Api event under a function's Events block), SAM generates the permission with the correct SourceArn for you. Letting the framework wire up the trigger is usually safer than hand-writing the permission.
How to prevent it from happening again
Manual fixes do not stick. Bake the check into the path that creates and changes Lambda functions.
Catch it in CI before deploy
Run a policy scanner against your Terraform plans or CloudFormation templates in the pipeline. Checkov, for example, flags Lambda permissions without source constraints. Add a step that fails the build on a violation:
checkov -d ./infra --framework terraform \
--check CKV_AWS_364
Write a policy-as-code guardrail
With Open Policy Agent and Conftest you can express the rule directly against Terraform plan JSON:
package lambda
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_lambda_permission"
principal := resource.change.after.principal
endswith(principal, ".amazonaws.com")
not resource.change.after.source_arn
msg := sprintf("Lambda permission '%s' grants %s without a source_arn", [resource.address, principal])
}
Detect drift across the fleet
Resource policies get edited outside IaC, especially when someone wires up a trigger from the console. Continuous scanning catches those. Lensix runs the lambda_nosourcearn check across every function in every connected account on a schedule, so a hand-added permission shows up as a finding without you having to remember to look.
Tip: Pair the scan with an EventBridge rule on AddPermission CloudTrail events. You get an alert the moment a new invoke permission lands, and you can immediately check whether it carries a source condition.
Best practices
- Always scope service principals. Any time the principal is an AWS service, add
SourceArn. Treat a permission without one as broken by default. - Add SourceAccount for global namespaces. S3 bucket names and some other identifiers are global. The extra
SourceAccountcondition prevents a deleted-and-recreated resource in another account from inheriting the trust. - Prefer framework-generated permissions. SAM, Serverless Framework, and the CDK generate scoped permissions when you declare the event source. Let them do it rather than writing raw permission statements.
- Use unique statement IDs. Clear, distinct
statement-idvalues make it obvious which trigger each permission belongs to and make targeted removal safe. - Review function URLs and public access separately. A missing
SourceArnis one exposure path. Function URLs withAuthType: NONEand overly open principals are others worth auditing in the same pass. - Keep execution roles tight. Even if an attacker triggers your function, a least-privilege execution role limits what the forged invocation can reach. Defense in depth matters here.
The cost of getting this right is one extra line in a permission statement. The cost of getting it wrong is an internet-reachable invocation path into your account. Add the SourceArn, gate it in CI, and scan for drift, and this whole class of confused deputy attack stops being a concern.

