This is less a front-door RCE and more a forgotten master key left in a retired lockbox
CVE-2024-28056 affects AWS Amplify CLI versions before 12.10.1. The bug appears when an Amplify project had its Authentication component removed: the CLI deleted the trust-policy Condition but left "Effect":"Allow", leaving legacy authRole and unauthRole IAM roles assumable through Cognito. Per AWS and Datadog, the risky state mainly applies to projects where auth was removed from apps built between August 2019 and January 2024; the fixed CLI shipped on January 10, 2024.
The vendor's 9.8/CRITICAL rating overstates today's enterprise risk. That score models a clean unauthenticated network path, but AWS subsequently changed IAM and STS in late February and April 2024 to block the straightforward cross-account abuse path. The residual risk is real, but it now depends on historical project state, left-behind vulnerable roles, and typically a same-account Cognito identity pool with classic authflow enabled. That's a meaningful downgrade in reachable population.
4 steps from start to impact.
Legacy Amplify role is created in a bad state
sts:AssumeRoleWithWebIdentity for cognito-identity.amazonaws.com but loses the aud scoping condition that should bind it to one identity pool.- Amplify CLI version earlier than
12.10.1was used historically - An Amplify project's Authentication component was removed
- The resulting IAM role still exists
- This only hits a subset of Amplify users, not every install
- It requires a specific lifecycle event, not just package presence
- Many orgs never removed Amplify auth at all
Attacker finds a usable role ARN and same-account Cognito path
- Knowledge of the target role ARN
- At least one Cognito identity pool in the victim account with
AllowClassicFlowenabled
- AWS blocked the clean cross-account route in STS
- Shodan/Censys-style internet scanning is mostly useless here because this is IAM/Cognito control-plane state
- Classic authflow is not universal in modern Cognito deployments
Attacker mints a Cognito token
amr=authenticated, the attacker needs to authenticate through a compatible user pool; if the trust policy has no aud scoping, the token can still be exchanged against the wrong role.- Victim identity pool supports classic flow
- For some variants, attacker can complete an authenticated Cognito flow
- Authenticated variants add setup and often require access to a working login path
- Broken or retired Cognito configs can make exploitation fail in practice
cognito-identity:GetId, GetOpenIdToken, and unusual identity-pool usage preceding STS role assumption.STS issues creds for the legacy role
sts:AssumeRoleWithWebIdentity against the leftover Amplify role. If successful, the attacker receives short-lived credentials and can act with whatever privileges the legacy authRole or unauthRole still holds, which can range from low-value app access to meaningful lateral movement in the AWS account.- Role trust policy remains permissive
- STS still accepts the same-account token-to-role chain
- Role policy grants useful permissions
- Blast radius is bounded by the role's actual permissions
- Well-scoped legacy roles may only expose one app's resources
- CloudTrail often records the key API calls needed for investigation
AssumeRoleWithWebIdentity; detections are strongest when you alert on Amplify-style role names or Cognito-trusted roles being assumed unexpectedly.The supporting signals.
| In-the-wild status | No CISA KEV entry found, and I found no authoritative reporting of active campaigns abusing this CVE at scale. Public research exists, but not confirmed broad exploitation. |
|---|---|
| Public exploit guidance | Yes. Datadog published original research and attack details, and *Hacking the Cloud* documented a same-account exploitation path after AWS blocked cross-account abuse. |
| EPSS | 0.00648 (~0.648%) from the prompt intel; that is low and consistent with a niche, environment-dependent attack path. I did not verify a primary-source percentile. |
| KEV status | Not listed in the CISA Known Exploited Vulnerabilities catalog. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H assumes a clean remote path. Post-mitigation reality is narrower: historical state + legacy IAM role + same-account Cognito classic flow. |
| Affected range | Amplify CLI < 12.10.1. Practically, exposure is tied to projects where the Authentication component was removed from apps built between 2019-08 and 2024-01. |
| Fixed versions and platform mitigations | CLI fixed in 12.10.1 on 2024-01-10. AWS says Amplify Studio was also fixed in January 2024; Datadog reports later STS/IAM changes in February and April 2024 blocked the broad cross-account path. |
| Exposure data | This is not a Shodan/Censys problem. Datadog instead used public code search and collected 8,000+ role ARNs in minutes, showing role-name discoverability is easy even though internet surface measurement is weak. |
| Disclosure and researcher | Coordinated disclosure landed 2024-04-15. Research was published by Nick Frichette / Datadog Security Labs. |
noisgate verdict.
The decisive factor is that AWS killed the easy cross-account exploit path, leaving a materially narrower same-account chain. What remains is a legacy cloud-state issue that usually requires additional Cognito conditions, so it is not a fleet-wide internet-critical event anymore.
Why this verdict
- Cross-account path is dead: AWS added STS protections in late February and April 2024 that block the original broad abuse path described by the CVSS story.
- Historical-state requirement cuts population hard: you only care if an older Amplify workflow removed auth and left behind a permissive Cognito-trusted IAM role.
- Residual exploit needs a second condition: practical abuse now typically requires a same-account Cognito identity pool with classic/basic authflow enabled, not just a vulnerable package version.
- Impact can still be meaningful: if the leftover role is privileged, successful
AssumeRoleWithWebIdentitybecomes AWS-account lateral movement, which keeps this above LOW.
Why not higher?
This is not a modern pre-auth internet smash-and-grab anymore. The clean remote path the vendor score implies has been constrained by AWS service-side changes, and the remaining chain depends on uncommon legacy configuration plus additional Cognito prerequisites.
Why not lower?
I would not drop this to LOW because the leftover IAM roles can still carry real privileges, and public role-ARN discovery is easier than many teams assume. If your account still has permissive Amplify roles and classic-flow identity pools, this becomes a practical privilege-escalation path.
What to do — in priority order.
- Audit Cognito-trusted IAM roles — Search for roles that allow
cognito-identity.amazonaws.comto callsts:AssumeRoleWithWebIdentitywithout anaudcondition. For a MEDIUM noisgate verdict there is no mitigation SLA; do this as part of the normal 365-day remediation window, but move faster if you find legacy AmplifyauthRoleorunauthRolenames. - Disable classic authflow on identity pools — Turn off Cognito basic/classic authflow where applications do not explicitly need it. That strips out the most important residual exploit enabler; for this MEDIUM case there is no mitigation SLA, so handle it in the normal remediation cycle unless you confirm a vulnerable role exists.
- Prune or re-scope legacy Amplify roles — Delete orphaned Amplify roles or reduce them to least privilege if the app still depends on them. This cuts blast radius even if a trust-policy issue slipped through; again, no mitigation SLA applies here, so fold it into the 365-day remediation plan.
- Alert on web-identity role assumptions — Create CloudTrail detections for
AssumeRoleWithWebIdentityagainst Cognito-trusted roles, especially Amplify-style role names. This gives you high-signal visibility into the exact final hop of the attack chain during the normal remediation window.
- Just upgrading the developer CLI does not repair already-created IAM roles; the bug leaves behind cloud state.
- Perimeter scanning, WAFs, and external attack-surface tools won't tell you much here because the exposure is in AWS IAM/Cognito trust relationships, not a public web endpoint.
- MFA on your human AWS admins is good hygiene but does not directly stop
AssumeRoleWithWebIdentityon a permissive Cognito-trusted role.
Crowdsourced verification payload.
Run this from an auditor workstation, AWS CloudShell, or CI job with boto3 available and read permissions to IAM, Cognito Identity, and optionally STS/EC2 region discovery. Invoke it as python3 check_cve_2024_28056.py --profile prod or with ambient credentials; it needs read-only AWS API access, not shell access to any host.
#!/usr/bin/env python3
# check_cve_2024_28056.py
# Exit codes:
# 0 = PATCHED (no vulnerable roles found)
# 1 = VULNERABLE (vulnerable role(s) + same-account classic-flow identity pool(s) found)
# 2 = UNKNOWN (insufficient permissions/errors, or vulnerable roles found but no classic-flow pool found)
import argparse
import sys
import json
from botocore.exceptions import BotoCoreError, ClientError
try:
import boto3
except Exception:
print('UNKNOWN - boto3 is not installed')
sys.exit(2)
def make_session(profile=None):
try:
return boto3.Session(profile_name=profile) if profile else boto3.Session()
except Exception as e:
print(f'UNKNOWN - unable to create boto3 session: {e}')
sys.exit(2)
def normalize_actions(action):
if isinstance(action, list):
return action
if isinstance(action, str):
return [action]
return []
def is_cognito_trust_vulnerable(stmt):
if stmt.get('Effect') != 'Allow':
return False
principal = stmt.get('Principal', {})
federated = principal.get('Federated')
if federated != 'cognito-identity.amazonaws.com':
return False
actions = normalize_actions(stmt.get('Action'))
if 'sts:AssumeRoleWithWebIdentity' not in actions:
return False
# Vulnerable pattern for this CVE family: missing aud scoping condition.
cond = stmt.get('Condition')
if not cond:
return True
string_equals = cond.get('StringEquals', {}) if isinstance(cond, dict) else {}
aud = string_equals.get('cognito-identity.amazonaws.com:aud')
if not aud:
return True
return False
def list_vulnerable_roles(session):
iam = session.client('iam')
vulnerable = []
paginator = iam.get_paginator('list_roles')
for page in paginator.paginate():
for role in page.get('Roles', []):
doc = role.get('AssumeRolePolicyDocument', {})
statements = doc.get('Statement', [])
if isinstance(statements, dict):
statements = [statements]
for stmt in statements:
if is_cognito_trust_vulnerable(stmt):
vulnerable.append({
'RoleName': role.get('RoleName'),
'Arn': role.get('Arn')
})
break
return vulnerable
def list_classic_flow_pools(session):
regions = session.get_available_regions('cognito-identity')
found = []
errors = []
for region in regions:
try:
client = session.client('cognito-identity', region_name=region)
token = None
while True:
kwargs = {'MaxResults': 60}
if token:
kwargs['NextToken'] = token
resp = client.list_identity_pools(**kwargs)
for pool in resp.get('IdentityPools', []):
pool_id = pool['IdentityPoolId']
desc = client.describe_identity_pool(IdentityPoolId=pool_id)
if desc.get('AllowClassicFlow') is True:
found.append({
'Region': region,
'IdentityPoolId': pool_id,
'IdentityPoolName': desc.get('IdentityPoolName', '')
})
token = resp.get('NextToken')
if not token:
break
except (ClientError, BotoCoreError) as e:
errors.append(f'{region}: {e}')
except Exception as e:
errors.append(f'{region}: {e}')
return found, errors
def main():
parser = argparse.ArgumentParser(description='Assess residual exposure for CVE-2024-28056 in an AWS account')
parser.add_argument('--profile', help='AWS profile name', default=None)
args = parser.parse_args()
session = make_session(args.profile)
try:
vulnerable_roles = list_vulnerable_roles(session)
except (ClientError, BotoCoreError) as e:
print(f'UNKNOWN - IAM enumeration failed: {e}')
sys.exit(2)
except Exception as e:
print(f'UNKNOWN - unexpected IAM error: {e}')
sys.exit(2)
try:
classic_pools, pool_errors = list_classic_flow_pools(session)
except Exception as e:
print(f'UNKNOWN - Cognito enumeration failed: {e}')
sys.exit(2)
summary = {
'vulnerable_role_count': len(vulnerable_roles),
'classic_flow_pool_count': len(classic_pools),
'example_roles': vulnerable_roles[:5],
'example_classic_flow_pools': classic_pools[:5],
'regional_errors': pool_errors[:10],
}
if vulnerable_roles and classic_pools:
print('VULNERABLE - permissive Cognito-trusted role(s) and same-account classic-flow identity pool(s) found')
print(json.dumps(summary, indent=2, sort_keys=True))
sys.exit(1)
if vulnerable_roles and not classic_pools:
print('UNKNOWN - vulnerable role(s) found, but no classic-flow identity pools were discovered')
print('UNKNOWN - cross-account abuse is blocked by AWS, but stale role trust policies still need review')
print(json.dumps(summary, indent=2, sort_keys=True))
sys.exit(2)
if not vulnerable_roles:
print('PATCHED - no vulnerable Cognito-trusted IAM role patterns found for CVE-2024-28056')
print(json.dumps(summary, indent=2, sort_keys=True))
sys.exit(0)
print('UNKNOWN - unable to determine state conclusively')
print(json.dumps(summary, indent=2, sort_keys=True))
sys.exit(2)
if __name__ == '__main__':
main()
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.