# The Danger of Multi-SSO AWS Cognito User Pools

Multi-SSO Cognito pools let a malicious IdP inject ghost identities and split usernames via underscore parsing, enabling privilege escalation and IdP routing hijack.

- Payout: Research
- Severity: high
- Program: Amazon
- Write-up by: Doyensec
- Disclosed: 2026-05-04
- Write-up quality: 8/10
- Original: https://blog.doyensec.com/2026/05/05/cloudsectidbits-masso-cognito-sso.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2026-05-05-cloudsectidbits-masso-cognito-sso-html
- Tags: area:cloud domain:cloud domain:web kind:research platform:research sev:high tech:aws tech:oidc tech:saml technique:parser-differential vuln:access-control vuln:auth-bypass vuln:business-logic vuln:privilege-escalation

**Target:** Cognito User Pool with multiple external OIDC/SAML IdPs and Lambda triggers
**Root cause:** Security checks placed on the wrong trigger (PreSignUp_ExternalProvider vs PostConfirmation/PreAuthentication) let a user record persist before domain validation, and positional split("_") parsing of the federated userName/sub allows guard/consumer parser differentials.
**Steps:**
1. Register a malicious OIDC IdP (EvilCorp) via the self-service SSO config page.
2. Federate with an attacker@company.com email; PreSignUp_ExternalProvider fires without the domain check, so Cognito persists the user record.
3. PostConfirmation JIT Lambda throws on the domain check and blocks the session, but the user record remains (ghost identity).
4. Abuse the persisted identity via other features, e.g. forceful password reset to gain non-SSO auth or impersonation.
5. For sub-splitting: send sub = `EVIL_noise_internal@company.com`; a guard using `sub.split("_")[1]` sees `noise` (passes), while a consumer using `sub.split("_")[-1]` stores `internal@company.com` as custom:primaryEmail.
6. For routing hijack: claim an unregistered IdpIdentifier (e.g. gmail.com) in self-service IdP config to redirect all users of that domain to the attacker IdP.
**How found:** Mapped the SSO trigger execution order and event types to find which triggers fire before user persistence, then reviewed how Lambdas parse event.userName/cognito:username and how IdpIdentifiers are exposed in self-service registration.
**Exploitation:** Attacker controls an external IdP registered in a shared Cognito pool. By federating with a victim-domain email, the user record is created before domain validation runs, leaving a durable ghost identity. Alternatively, crafted sub values with underscores exploit inconsistent split parsing between the PreSignUp guard and JIT provisioning consumer, writing attacker-chosen security-sensitive attributes (custom:primaryEmail, tenantID, role). Claiming an unregistered IdpIdentifier redirects all users of that domain to the attacker's IdP.
**Escalation:** Ghost identity can be pivoted to password reset for non-SSO auth or impersonation; sub-splitting writes attacker-controlled custom attributes consumed as tenant/role, yielding privilege escalation.
**Insight:** In multi-tenant Cognito, PreSignUp_ExternalProvider is the only gate before user creation; checks in PostConfirmation/PreAuthentication leave a durable ghost identity. Also audit every split("_") on userName/sub for guard-vs-consumer positional index mismatches, since ProviderName forbids underscores but sub does not.

## Proof of concept (copied exactly from the original report)

**Sub-splitting attack payload and parser differential**

```
Sending sub = EVIL_noise_internal@company.com from the malicious IdP would result in:

 Lambda
 Code
 Index
 Sees

 pre_signup (uniqueness guard)
 sub.split("_")[1]
 second token
 "noise" not in pool, passes

 jit_provisioning (consumer)
 sub.split("_")[-1]
 last token
 "internal@company.com", stored as custom:primaryEmail
```

**PreSignUp triggerSource branching fix pattern**

```
def lambda_handler(event, context):
 if event["triggerSource"] in (
 "PreSignUp_SignUp",
 "PreSignUp_ExternalProvider",
 "PreSignUp_AdminCreateUser",
 ):
 enforce_domain_policy(event["request"]["userAttributes"]["email"])
 return event
```

**Federated username format with underscore**

```
<ProviderName>_<sub>


This appears as event.userName in triggers and as cognito:username in tokens.
```

