Introduction
During a recent AWS penetration test, I discovered an authorization bypass in an application built withAWS Amplify. The application used Amazon Cognito for authentication and AWS AppSync for its GraphQL API, with group-based access control restricting sensitive endpoints to admin users only. On the surface, the access control looked solid — self-registered users were correctly denied access to privileged API actions.
But the application had a second authorization path that nobody had considered. By exchanging my Cognito JWT for temporary AWS credentials through the Cognito Identity Pool and signing requests with SigV4, I was able to call the same admin-restricted API endpoints as a low-privilege user — completely bypassing the Cognito group check. When I searched for existing research on this issue, I couldn't find any public writeups documenting this vulnerability pattern.
This post walks through how the vulnerability works, why AWS Amplify makes it easy to introduce, and how to test for it. I created a CTF lab to demonstrate the vulnerability, which is live at ctf.splintercat.xyz if you'd like to try it yourself. I also tested it against AWS's new AI Security Agent to see how it handles this class of vulnerability.
Background: How Amplify Sets Up Authorization
To understand this vulnerability, you need to understand how AWS Amplify wires together authentication and authorization. Amplify is a framework that automates the provisioning of cloud resources for web and mobile applications. When you add authentication and a GraphQL API to an Amplify project, it provisions several interconnected services:
Amazon Cognito User Pool — The primary identity provider. Users register, sign in, and receive JWT tokens. Group membership (like an "admins" group) is embedded in the JWT as the cognito:groups claim.
Amazon Cognito Identity Pool — A separate service that federates with the User Pool. After a user authenticates and receives a JWT, the Identity Pool can exchange that JWT for temporary AWS credentials (an access key, secret key, and session token). These credentials are tied to an IAM role — the "authenticated role" — that Amplify auto-generates during setup.
AWS AppSync — The managed GraphQL API. AppSync supports multiple authorization modes simultaneously. The two relevant modes here are:
- Cognito User Pool (JWT) — AppSync validates the JWT and checks claims like
cognito:groupsto enforce group-based access control. - IAM (SigV4) — AppSync validates that the request is signed with valid IAM credentials that have
appsync:GraphQLpermissions. No group check occurs.
The critical detail is that Amplify's auto-generated authenticated role has appsync:GraphQL permissions on the API. This role is granted to every authenticated user who exchanges their JWT through the Identity Pool — regardless of which Cognito groups they belong to.
The Real-World Discovery
For the application that I was testing, the intended design was straightforward:
- Any user could self-register and create an account.
- New users had to be reviewed and assigned to a role by an administrator before they could access any data or perform actions.
- The GraphQL schema enforced this using Cognito User Pool group-based authorization — only users in privileged groups could access sensitive queries and mutations.
As a newly registered user, the frontend correctly denied access. API requests returned "Unauthorized" because my JWT didn't contain the required group membership.
But I had read-only access to the AWS environment as part of the scope. When I reviewed the AppSync authorization configuration, I noticed the API accepted both Cognito User Pool and IAM authorization modes. And when I looked at the GraphQL schema, I saw the pattern:
@auth(rules: [
{ allow: groups, provider: userPools, groups: ["admins"] }
{ allow: private, provider: iam }
])The first rule does what you'd expect — restricts access to the "admins" group via JWT claims. The second rule allows any IAM-authenticated caller to access the endpoint. Combined with the fact that the Cognito Identity Pool gives every authenticated user temporary IAM credentials tied to the authenticated role, this creates a bypass.
When recreating the application with the same authorization pattern, the auto-generated resolver logic shows how AppSync evaluates the two auth paths independently. The IAM path (top block) grants access to any caller whose credentials match the Identity Pool's authenticated role — which includes every authenticated user. The User Pool path (bottom block) checks the JWT for "admins" group membership. Both paths lead to the same result: $isAuthorized = true.

The resolver also reveals the infrastructure wiring that makes this possible. The stash context template below shows that the Identity Pool ID and the authenticated role ARN are hardcoded directly into the resolver — this is what Amplify auto-generates when you add provider: iam to an @auth directive, and it's what allows the IAM path to authorize any user who assumes that role.

Demonstrating the Bypass
To demonstrate this vulnerability in a controlled environment, I built a simple Amplify application that mirrors the authorization pattern I found. The scenario: a company has an internal dashboard where administrators can view sensitive configuration data. Only users in the "admins" Cognito group are supposed to access this data, but the GraphQL schema includes IAM as a secondary authorization mode. Any user can self-register, but they'll be denied access through the frontend.
The Exploit Chain
The exploit is straightforward once you understand the architecture:
Step 1: Authenticate and Get a JWT
Register an account and authenticate with Burp Suite proxying the traffic. During the login process, the browser automatically performs two actions: it authenticates against the Cognito User Pool and receives a JWT, and then it immediately exchanges that JWT with the Cognito Identity Pool for temporary AWS credentials.


Step 2: Confirm Access is Denied via JWT
Click the "View Config" button. The frontend sends the GraphQL query with authMode: "userPool", and AppSync checks the JWT's cognito:groups claim. Since you're not in the "admins" group, it returns Unauthorized.


Step 3: Extract the IAM Credentials
The temporary AWS credentials captured in Burp can be exported directly into an AWS CLI profile and used outside the application. Verify they belong to the Cognito Identity Pool's authenticated role:
aws configure --profile cognito-auth-bypass
aws sts get-caller-identity --profile cognito-auth-bypass
Step 4: Call the Admin API via IAM
The frontend uses JWT authorization for all API requests, which is why our non-admin user was denied in Step 2. Using awscurl, we can send a SigV4-signed request directly to the AppSync endpoint using the temporary credentials we extracted. SigV4 is AWS's request signing protocol — it authenticates the request using the IAM credentials rather than a JWT, which means AppSync evaluates the IAM authorization rule instead of the Cognito group check.
awscurl --service appsync --region ca-central-1 \
--profile cognito-auth-bypass \
-X POST "https://[APPSYNC_ENDPOINT]/graphql" \
-H "Content-Type: application/json" \
--data '{"query":"query { getAdminConfig { ok message data } }"}'The response comes back successfully — in this case, dummy configuration data seeded for demonstration purposes. The key detail is in the message field: "Access granted via: IAM (SigV4)", confirming the request bypassed the Cognito group check entirely and was authorized through the IAM path instead.

Why This Happens
The root cause is a mismatch between how developers think about authorization and how Amplify actually implements it.
What the developer intended: "Only users in the admins group can access this endpoint. I'll also add IAM auth so our backend services can call the API for monitoring and audit purposes."
What actually happens: The { allow: private, provider: iam } rule tells AppSync to accept any request signed with valid IAM credentials that have appsync:GraphQL permissions. The developer assumed this would only apply to backend service roles, but Amplify's Identity Pool gives every authenticated user temporary IAM credentials tied to the authenticated role — and that role has appsync:GraphQL permissions auto-generated by Amplify.


The Cognito Identity Pool doesn't differentiate between users based on their User Pool group membership. Whether you're an admin or a freshly self-registered user with no group assignments, you get the same authenticated IAM role. The group-based access control only works on the JWT path — the IAM path bypasses it entirely.
This is particularly insidious because:
- The frontend works correctly. Non-admin users see "Access Denied" because the frontend uses JWT auth.
- The Amplify CLI generates the IAM role and its permissions automatically. The developer never explicitly grants
appsync:GraphQLpermissions — it happens behind the scenes. - Adding
provider: iamto an@authdirective looks innocuous.
Testing with AWS Security Agent
I was curious how AWS's new AI Security Agent would handle this vulnerability, so I ran it against the CTF lab using a few different approaches. I wanted to see if the agent could detect the issue in a blackbox scenario before giving it any whitebox context.
Penetration Test
AWS Security Agent requires DNS verification for any target domain, so I configured the necessary records for my custom domain and pointed the agent at the application. I had self-registration enabled and anticipated that the agent might struggle with the Amplify Authenticator login flow, so I attached a Lambda trigger that auto-confirms new users without requiring email verification.
The agent ran for nearly 8 hours against my application. It executed a comprehensive list of standard web vulnerability checks — XSS, SQL injection, path traversal, SSRF, JWT manipulation, SSTI, and more. I repeatedly checked my Cognito User Pool throughout the run to see if the agent had attempted to register a user, but it never did.

After reviewing the logs, I discovered why: the agent's infrastructure uses a proxy that blocks outbound connections to *.amazonaws.com endpoints. Since both Cognito and AppSync live on AWS-managed domains, the agent couldn't interact with either backend service. It also refused to test anything outside the domain I had verified through DNS, further limiting its ability to interact with the application's backend.
This is a limitation: the agent's domain-based scoping model is incompatible with cloud-native applications where the backend lives entirely on AWS-managed service endpoints. The agent can scan the static frontend, but it can't test the authorization logic that actually matters.
Interestingly, the agent did appear to send at least one request to an out-of-scope endpoint during the run, as shown in the logs.


The total task hours across all agents came to just under 29 hours. At AWS's listed rate of $50 per task hour, the total cost for this run would be approximately $1,500 — for an application with a single API endpoint.

Findings
The agent produced three findings, none of which identified the actual authorization bypass.

Source Map Exposes AWS Configuration (High) — The agent found that the JavaScript source map was publicly accessible and contained the full aws-exports.js configuration. This is a legitimate finding, but the severity is overstated. The values exposed (User Pool ID, Identity Pool ID, AppSync endpoint) are designed to be client-side — they're present in the compiled JavaScript bundle regardless of whether the source map exists.
Weak Cognito Configuration — MFA Disabled (Informational) — The agent noted that MFA was disabled and the password policy was minimal. A fair observation, though not particularly interesting.
Cognito Identity Pool Allows Unauthenticated Guest Access (Informational) — This was a false positive. The agent analyzed the Amplify SDK source code in the JavaScript bundle and determined that because aws_mandatory_sign_in was absent from the configuration, the SDK's internal logic would compute allowGuestAccess as true. It even claimed to have observed successful unauthenticated GetId calls to the Identity Pool. However, guest access was explicitly disabled in the Identity Pool configuration — the agent inferred the vulnerability from client-side code analysis without actually confirming it server-side, and got it wrong.

What makes this last finding interesting is that the agent was looking at the right components. It identified the Identity Pool, understood that it provides IAM credentials, and even noted that those credentials could be used to call the AppSync API. But it framed it as an unauthenticated guest access issue rather than recognizing the real vulnerability — that authenticated users receive IAM credentials that bypass the Cognito group check. It was one logical step away from the actual finding, and I suspect it would have identified the bypass if it had been able to actually authenticate and interact with the backend services.

Design Review
The security agent also offers a design review feature where you upload architecture documentation and it evaluates compliance against security requirements. I submitted a design document describing the application's architecture, including the dual authorization configuration on AppSync, the Cognito Identity Pool providing credentials to authenticated users, and the IAM provider being included on the getAdminConfig query.
The agent correctly identified the authorization bypass. It flagged the finding as non-compliant, described the full exploit chain — noting that any self-registered user could obtain IAM credentials through the Identity Pool and invoke the sensitive query via SigV4 signing — and provided remediation guidance recommending either scoping the IAM rule to a dedicated backend role or removing it entirely.


Key Takeaways from Security Agent Testing
The security agent demonstrated knowledge of the right attack patterns but was prevented from executing them by its own infrastructure constraints. The domain-based scoping model is incompatible with testing cloud-native applications where the backend lives on AWS-managed service endpoints. And the design review's effectiveness depends heavily on how explicitly the architecture documentation describes the authorization configuration.
For this class of vulnerability, manual testing with cloud-specific knowledge remains the most reliable detection method.
Detection and Remediation
How to Check for This
If you're assessing an Amplify application, look for these indicators:
- Check the AppSync authorization configuration. If the API has both Cognito User Pool and IAM authorization modes enabled, there may be a bypass path.
- Review the GraphQL schema for
provider: iamrules. Any@authdirective that includes{ allow: private, provider: iam }on a sensitive endpoint is potentially vulnerable. - Check the Identity Pool configuration. If an Identity Pool is federated with the User Pool and uses a single authenticated role for all users (which is Amplify's default), then IAM credentials are available to every authenticated user.
- Test it. Authenticate as a low-privilege user, extract the temporary IAM credentials from the browser traffic (or exchange your JWT via the CLI), and try calling the protected endpoint with
awscurl. If the query returns data that should be restricted, the bypass is confirmed.
Remediation
Option 1 — Remove IAM auth from sensitive queries. The simplest fix. If no backend services actually need IAM access to the endpoint, remove the IAM rule entirely:
@auth(rules: [
{ allow: groups, provider: userPools, groups: ["admins"] }
])Option 2 — Use role mapping in the Identity Pool. A more robust approach. Configure the Identity Pool to assign different IAM roles based on the cognito:groups claim. Admin users get a role with AppSync permissions; regular users get a role without them. This preserves IAM access for legitimate use cases while preventing unprivileged users from exploiting it.
Option 3 — Add resolver-level authorization. If IAM access is genuinely needed for backend services and role mapping isn't feasible, add a check in the Lambda resolver to validate the caller's identity and reject requests that come from the Identity Pool's generic authenticated role rather than a dedicated service role.
Conclusion
This vulnerability exists because AWS Amplify makes it easy to add IAM authorization without understanding the implications. The { allow: private, provider: iam } directive looks like a simple way to enable backend access, but it actually opens a parallel authorization path that bypasses all group-based access control. The Cognito Identity Pool provides the IAM credentials, Amplify auto-generates the permissions, and the developer never sees a warning.
If you're building or testing Amplify applications, check your @auth directives. If you see provider: iam on a sensitive endpoint and your Identity Pool uses a single authenticated role, you likely have this vulnerability.