DEV Community

Bala Paranj
Bala Paranj

Posted on

The Lambda Trigger You Deleted is Still Authenticating Users

✓ Human-authored analysis; AI used for formatting and proofreading.

You deleted a Lambda function six months ago. The code is gone. The CloudWatch logs stopped. The function doesn't appear in the Lambda console.

But the Cognito user pool still references it. Every authentication attempt invokes a trigger that points to a function that doesn't exist. Depending on which trigger it is, the result ranges from silent authentication failures to bypassed validation logic.

Your scanner doesn't flag this. The Lambda check says "function doesn't exist" (correct, you deleted it). The Cognito check says "trigger is configured" (correct, the reference was never cleaned up). Neither check connects the two facts: the trigger points to nothing.

That's a ghost reference. It's breaking your authentication flow right now.

Ghost References

A ghost reference is a configuration that points to a resource that no longer exists. The pointer remains after the target is deleted. AWS doesn't clean up cross-service references automatically. That's the customer's responsibility under the shared responsibility model.

Ghost references are everywhere in cloud infrastructure:

  • A Cognito user pool trigger pointing to a deleted Lambda function
  • An S3 event notification targeting a deleted SNS topic
  • An API Gateway integration pointing to a deleted Lambda
  • A CloudWatch alarm sending to a deleted SNS subscription
  • A Route 53 CNAME pointing to a decommissioned S3 bucket
  • An IAM role trust policy allowing a deleted account to assume it

Each one is a configuration that references something that isn't there. The reference isn't invalid in the syntactic sense. The ARN is well-formed, the configuration passes schema validation. It's invalid in the semantic sense. The target doesn't exist, so the reference does nothing, does the wrong thing, or creates a security gap.

Cognito triggers and what happens when they're ghosts

Cognito user pools support Lambda triggers at multiple points in the authentication flow:

Trigger When it fires What a ghost does
Pre Sign-up Before a new user is registered Ghost: registration proceeds without custom validation. Blocked users can register.
Pre Authentication Before credentials are verified Ghost: authentication proceeds without custom checks. IP blocklists, risk scoring, device validation are skipped.
Post Authentication After successful login Ghost: audit logging, session enrichment, and downstream notifications don't fire. Login succeeds but nobody knows.
Custom Message When Cognito sends email/SMS Ghost: messages fail silently or use default templates that may lack required compliance text.
Pre Token Generation Before tokens are issued Ghost: custom claims aren't added to tokens. Downstream services that check custom claims may fail or grant wrong access.
Post Confirmation After email/phone verification Ghost: onboarding workflows don't execute. New users exist but aren't provisioned in downstream systems.
Define Auth Challenge For custom auth flows Ghost: custom auth flow breaks. Users can't authenticate through the custom path.
Create Auth Challenge For custom auth flows Ghost: same as above. Custom challenges can't be created.
Verify Auth Challenge For custom auth flows Ghost: challenge responses can't be verified. Either authentication fails or falls back to a less secure path.

The Pre Sign-up and Pre Authentication ghosts are the most dangerous. A deleted Pre Sign-up trigger means every registration validation rule you implemented in that Lambda such as email domain restrictions, blocklist checks, CAPTCHA verification is gone. Users your validation would have blocked can now register freely.

A deleted Pre Authentication trigger means every login-time check such as IP reputation, device fingerprinting, concurrent session limits, brute-force detection is gone. The authentication succeeds without any of the custom security logic you built.

Why this happens

Lambda functions get deleted for legitimate reasons:

  • A developer refactors the auth flow and deploys a new function with a different name
  • An automated cleanup script removes functions with no recent invocations (the ghost trigger means no invocations, which triggers the cleanup, which ensures the ghost persists)
  • A team migrates from Lambda-based triggers to Cognito's built-in features but forgets to remove the trigger configuration
  • An infrastructure-as-code update removes the Lambda resource but doesn't update the Cognito user pool resource

The Cognito user pool doesn't validate that trigger targets exist. It stores the ARN and attempts to invoke it. If the function doesn't exist, the invocation fails. What happens next depends on the trigger type and the pool's error handling configuration.

The feedback loop that hides the problem

Ghost triggers create a self-concealing feedback loop:

  1. Lambda is deleted → trigger invocation fails
  2. CloudWatch logs nothing (the function doesn't exist, so there are no invocation logs)
  3. Cognito may or may not log the trigger failure (depends on advanced security settings)
  4. Authentication succeeds or fails silently (depends on trigger type)
  5. No alert fires (because the monitoring was on the Lambda function, which is deleted)
  6. Nobody notices (because the dashboard checks "is the trigger configured?" — yes — and "does the Lambda exist?" — checked against the Lambda console, which shows nothing)

The two checks exist in different dashboards. The Cognito dashboard shows a trigger is configured. The Lambda dashboard shows no function. Nobody connects the dots because the tools check each service independently.

What compound detection finds

Analyzing the full configuration snapshot consisting of Cognito user pool AND Lambda function inventory together reveals what per-service checks miss.

Ghost reference detection compares every Cognito trigger ARN against the Lambda function inventory. If the trigger points to a function that doesn't exist in the snapshot, it's a ghost. This is a cross-service join: Cognito configuration references Lambda inventory. No single-service check can perform this join.

For a user pool with four ghost triggers (Pre Sign-up, Pre Authentication, Custom Message, Post Confirmation), static analysis finds:

Four individual ghost findings One per trigger type, each explaining what security logic is bypassed:

[HIGH] Pre Sign-up trigger references deleted Lambda
       → Registration validation is bypassed. Custom blocklists,
         email domain restrictions, and CAPTCHA checks are not enforced.

[HIGH] Pre Authentication trigger references deleted Lambda
       → Login-time security checks are bypassed. IP reputation,
         device fingerprinting, and brute-force detection are not enforced.

[MEDIUM] Custom Message trigger references deleted Lambda
         → Messages use default Cognito templates. Compliance-required
           text may be missing from verification emails.

[MEDIUM] Post Confirmation trigger references deleted Lambda
         → Onboarding workflows don't execute. New users exist in
           Cognito but may not be provisioned in downstream systems.
Enter fullscreen mode Exit fullscreen mode

One compound chaincognito_ghost_authflow fires when multiple ghost triggers exist on the same user pool. The compound is more severe than any individual ghost because it means the entire custom auth flow is absent, not just one check. The authentication pipeline is running on Cognito's defaults with zero custom security logic.

SMT satisfiability checking asks: "Is there a user pool where a ghost trigger exists AND the trigger type is Pre Authentication?" Answer: sat. Witness: the specific user pool and trigger ARN. After cleanup (removing the stale trigger configuration), the query returns unsat where no ghost references remain.

The fix

Two options:

Option A: Remove the stale trigger (if the Lambda is no longer needed)

# Remove the trigger from the user pool configuration
aws cognito-idp update-user-pool \
  --user-pool-id us-east-1_xxxxx \
  --lambda-config '{}'
Enter fullscreen mode Exit fullscreen mode

This clears all triggers. If some triggers are still valid, specify only those in the lambda-config JSON and omit the stale ones.

Option B: Re-create the Lambda (if the trigger logic is still needed)

# Deploy a new function with the same logic
# Update the trigger to point to the new function ARN
aws cognito-idp update-user-pool \
  --user-pool-id us-east-1_xxxxx \
  --lambda-config '{"PreSignUp": "arn:aws:lambda:us-east-1:123:function:new-presignup"}'
Enter fullscreen mode Exit fullscreen mode

Option A is appropriate when the custom logic was replaced by Cognito's built-in features (e.g., built-in email verification replaced a custom verification Lambda). Option B is appropriate when the custom logic is still needed and was accidentally deleted.

How to check your own environment

# List your user pools
aws cognito-idp list-user-pools --max-results 20

# For each pool, get the trigger configuration
TRIGGERS=$(aws cognito-idp describe-user-pool \
  --user-pool-id <pool-id> \
  | jq -r '.UserPool.LambdaConfig | to_entries[] | .value')

# For each trigger ARN, check if the Lambda exists
for arn in $TRIGGERS; do
  func_name=$(echo "$arn" | grep -oP '(?<=function:)[^:]+')
  exists=$(aws lambda get-function --function-name "$func_name" 2>&1)
  if echo "$exists" | grep -q "ResourceNotFoundException"; then
    echo "GHOST: $arn — Lambda does not exist"
  else
    echo "OK:    $arn"
  fi
done
Enter fullscreen mode Exit fullscreen mode

If any trigger prints GHOST, you have a ghost reference. The authentication logic that trigger was supposed to enforce is not running.

The broader pattern

Ghost references are not a Cognito problem. They're a lifecycle problem. Every cloud service that references another service creates the possibility of a ghost when the target is deleted without updating the reference.

The most dangerous ghosts are the ones in the authentication path. Because they silently disable security logic instead of visibly breaking functionality. A ghost S3 event notification is a missed alert. A ghost Cognito Pre Authentication trigger is a disabled security check that nobody notices for months.

The tools that check each service independently will never find ghosts. A ghost is by definition a cross-service inconsistency: the reference exists in Service A, the target doesn't exist in Service B. Finding it requires comparing the configuration of both services in the same analysis. That's a compound check and it's the check your scanner doesn't perform.


Ghost reference detection is a core capability of Stave, an open-source static analysis tool with 3,000+ controls across 100+ AWS service domains. Stave evaluates cloud configurations via CEL predicates and exports standardized facts for consumption by external reasoning engines all from air-gapped snapshots without any cloud credentials.

Top comments (0)