by: Andrea Cocco, Security Consultant [Twitter,Linkedin] date: 01/07/2022

On June 3, 2022, a new feature was introduced for user pools within the AWS Cognito console: Verifying attribute changes. Within this feature, there is the check: Keep original attribute value active when an update is pending – Recommended, which is, at the time of writing, selected by default for new user pools. This check prevents Cognito from updating a user attribute (for example the e-mail) without verification.

This feature, however, was not present before June 3, 2022, therefore any user pool created before this date, might potentially be subject to the risks described in this article. It is therefore recommended to verify that your user pool, if you use “Cognito-assisted verification and confirmation”, has this option selected.

Our team is always looking for new vulnerabilities, even in Cloud environments. In this article, inspired by a real case encountered recently during a penetration test, we will show how a default (at the time of the pentest) AWS Cognito (mis)configuration can easily lead to a horizontal authorization bypass vulnerability.

For confidentiality reasons, and in order to carry on further analysis, we have reproduced in our laboratory an example that simulates the situation encountered, which was logically similar (albeit different). It is also possible to reproduce a similar scenario by using Amplify and importing the Auth module (Amplify uses Cognito for authentication).

TL;DR

A Cognito misconfiguration (due to the lack of the “Keep original attribute value active when an update is pending – Recommended” option) can lead to an authorization bypass vulnerability. Although at the time of writing this option is present and selected by default, it was not available before June 3, 2022, so any user pool created before this date, might still be subject to the risks described in this article. The following points represent an example of vulnerable scenario:

  • The application is using AWS Cognito for authentication, using an e-mail address for sign-up and sign-in
  • The application shows different content based on the employees roles. It does so by checking the e-mail subdomain and applying domain aliasing and subdomain stripping techniques
  • Cognito-assisted verification and confirmation is implemented, allowing users to change their e-mails with arbitrary new e-mails without verifying them
  • A malicious user can call the Cognito APIs and change his/her email with a fake e-mail, gaining unauthorized access to content in the application

Spoiling the solution:

Make sure the option “Keep original attribute value active when an update is pending – Recommended” is selected. In this way, if users change their email, they will not be able to use it without validation, preventing the possibility to log in with fake/arbitrary e-mails and, as in this case, bypass the authorization schema.

AWS Cognito user pool and user account confirmation

When setting up a user pool (and when dealing with sign-up and authentication in general), it is recommended to require a contact method belonging to the users in order to:

  • verify the identity of the user
  • send temporary token when users want to reset their passwords
  • send promotional messages
  • send account summaries or billing reminders

It is possible to confirm users with Cognito in three different ways: Lambda triggers, Admin confirm or Confirm via email/phone. One of the most frequently used is Confirm via email/phone through the “Cognito-assisted verification and confirmation”.

When creating a user pool, Cognito asks if you want to use the “Cognito-assisted verification and confirmation” option to verify the contact method for your users. With this enabled, Cognito will automatically send a message to the user contact method chosen for sign-up. The code included inside the message is then used to confirm the contact method (attribute) and validate the user. If a user does not input the received code (OTP), despite he is signed up, he cannot sign in the application, and he remains in an unconfirmed state.

The following diagram illustrates the confirmation process:

Reference: AWS managing users

Exploiting the Cognito User Pool contact verification and confirmation process

As previously stated, the following is the reconstruction of a web application penetration test we performed recently. We have recreated the same conditions present in the target application in our local laboratory. Please note that this should be only considered as a proof of concept and therefore the application that resides behind the login page has only simple functionalities for demonstration purposes. The customer application, on the other hand, was more complex.

The target application presents a login page from which it was possible to sign up and sign in the application. The company’s staff was assigned company’s emails with different subdomains depending on which department they are working in. Domain aliasing and subdomain stripping techniques were implemented, and different subdomains would grant access to different content (intended for the desired department).

As an example, if we log in with the account: john@marketing.bedefended.com we are presented with content specific for the marketing staff.

Trying to log in as Eve, from the HR department, we only see content related to the HR.

We then decided to test the sign-up process in order to check if, and how, the application verifies the users emails.

Upon registration, a code is sent to the e-mail of the user that is signing up in order to validate such e-mail. This appears to be the standard Cognito-assisted verification and confirmation process, by which Cognito automatically sends the code to the user e-mail for verification.Only after entering the OTP, we are able to log in the application.

The important things to note so far are:

  • The application uses AWS Cognito for authentication using the e-mail as the login required attribute
  • The application provides different content based on the e-mail subdomain
  • The application uses the e-mail as a required attribute for sign-up and appears to be using the “Cognito-assisted verification and confirmation” option to automatically send OTP and verify users

At this point,in order to be able to do something, we need at least an access token. To do so, we intercept the login request:

We can use the “AccessToken” with the AWS CLI to directly call the get-user Cognito API and get the information belonging to the account we are currently authenticated with.

By calling the preceding API we made sure that we are logged in as eve@hr.bedefended.com and that we can indeed call the Cognito APIs. Proceeding further we decide to attempt to call the Cognito APIs in order to directly change the email address. Since the Eve user only has the @hr.bedefended.com e-mail address, we should expect that we won’t be able to verify the modified e-mail address and hence, not be able to log in the application with that e-mail. We can try change the e-mail address using the update-user-attributes action.

Now we can call again the get-user action to check the new information

As we can see from the above screenshot, the email has been changed, and it is now in the Registered (unconfirmed) state as shown by the key-value pairs: “email_verified” : “false”. The next step is trying to log into the application using the self-changed, unverified e-mail address. As can be seen from the following screenshots, we found that it is indeed possible to log in with the new email, and by doing so, gaining unauthorized access to private content.

Conclusion and suggestions: how to fix the AWS Cognito account verification flow

In a scenario like the one described above, the first thing to keep in mind and remedy is the authorization method. In fact, an authorization schema based solely on the domain (or subdomain) of the email is not sufficiently secure and a defense-in-depth approach should be implemented instead.

The following are some suggestions to mitigate the issue on the AWS Cognito side.

When the “Cognito-assisted verification and confirmation” option is chosen, it does validate the user contact at sign-up (as we saw when we registered the new user), in fact, if we didn’t validate the e-mail with the received OTP, we would not have been able to log in. However, once a user has validated their contact once, then they can arbitrarily change the e-mail, even with a fake one, without the need to validate it.

It seems that the definitive remediation for this issue would be to forbid a user from changing their e-mail altogether. However, the required attributes in an AWS Cognito user pool are always writable (at least at the time of writing). This means that if the e-mail is a required attribute for registering into the application, it will always be possible to change it, even if there isn’t a front-end interface for carrying out such operation.

While it is not possible to prevent a user from changing his or her email, it is now possible to force them to validate the new e-mail. Recently, in fact, in order to fix such security concerns (among other usability problems), a new section called: Verifying attribute changes was introduced in the Cognito console. To reach it, go to the Cognito console, select the user pool, “Sign-up experience” tab and then under the section: Attribute verification and user account confirmation click on “edit”. The following options will be presented:

Make sure the option: “Keep original attribute value active when an update is pending – Recommended” is selected (at the time of writing it is selected by default when creating a new user pool, however it was not present before June 3, so if your user pool was created before that date, make sure to select it). In this way, if a user change their email, they will not be able to use it without a verification (by using the OTP received on the new email) and, until the new email is verified, the user can still continue to use the previous email. Under Active attribute values when an update is pending, choose the attributes that users need to verify before Amazon Cognito updates their value (in the case of our application would be the e-mail address). The same result can be achieved also programmatically using the AWS Cognito SDK, and setting the RequireAttributesVerifiedBeforeUpdate parameter in an UpdateUserPool request.

After receiving the OTP on the new e-mail, the app should call the VerifyUserAttributes API to update the affected attribute to its pending value:

Cognito API call: VerifyUserAttributes – request

Host: cognito-idp.eu-central-1.amazonaws.com
Content-Length: 1137
Cache-Control: max-age=0
Sec-Ch-Ua: "-Not.A/Brand";v="8", "Chromium";v="102"
X-Amz-User-Agent: aws-amplify/5.0.4 js
Content-Type: application/x-amz-json-1.1
X-Amz-Target: AWSCognitoIdentityProviderService.VerifyUserAttribute
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Accept: */*
Origin: http://localhost:3000
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
                  
{"AccessToken":"eyJra[...]","AttributeName":"email","Code":"[CODE]"}

Cognito API call: VerifyUserAttributes – response

HTTP/2 200 OK
Date: Mon, 06 Jun 2022 13:32:40 GMT
Content-Type: application/x-amz-json-1.1
Content-Length: 2
X-Amzn-Requestid: 94ab0499-0faf-41ef-a4c8-289e5fd28d78
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: x-amzn-RequestId,x-amzn-ErrorType,x-amzn-ErrorMessage,Date
                  
{}

After making this API call with the code received by email, the old email will no longer be usable and only the new one, verified, can be used. This would be sufficient to prevent the authorization bypass found in the application we have tested, since a malicious user, even if he attempted to change his e-mail with a fake one, would not be able to verify it and access the application with it.

Reference: AWS user pool settings

Be aware of the security of your AWS Cognito configuration. BeDefended.

Thanks for reading.