This is the third article in the Hacking AWS Lambda Functions series. The first arcticle touched common AWS Lambda vulnerabilities and best practices to avoid them, and the second article applied the theory to concrete examples of vulnerable serverless applications.
This article highlights the importance of proper token validation in serverless architectures, especially when using services like Amazon Cognito. It emphasizes the need for multiple security layers, including API Gateway authorizers and server-side token validation, to protect against unauthorized access and data exposure. The journey continues with the DVSA. Now, it’s time to dig deeper into Cognito token issues.
Manipulating the Cognito Access Token
Amazon Cognito issues tokens in JSON Web Token (JWT) format. Let’s examine the access token provided by Cognito and use a tool offered by jwt.io:
AWS Cognito JWT Token
Note the warning on the page: “JWTs are credentials, which can grant access to resources. Be careful where you paste them! We do not record tokens; all validation and debugging is done on the client side.” Keep this in mind, and don’t copy-paste them mindlessly to random web forms asking you to do so.
As we can see from the decoded token, it consists of three parts: a header, payload, and signature, each base64 URL encoded (note that you need to omit the padding with “=” per https://tools.ietf.org/html/rfc7515#section-2) and separated by the dots:
- Header: Consists of the key identifier and the algorithm used. RS256 (RSA Signature with SHA-256) is an asymmetric algorithm using public and private key pairs. The identity provider, here Amazon Cognito, uses a private key to generate the signature, and the receiver of the JWT uses a public key to validate the JWT signature.
- Payload: The actual content; these attributes are called claims. The standard defines some, but you can also add your own with Amazon Cognito.
- Signature: The signature is calculated based on the header and payload sections and a secret. Signing JWTs doesn’t make their data unreadable! It is used to verify that the JWT issuer is who it says it is and to ensure that the message wasn’t changed along the way.
If we change the payload or the header, the calculated signature will be invalid, and the token should not be allowed, right? Let’s try to change our username and sub-claims (it seems like the user identifier is also used for the username) to another user with some purchases:
echo '{"sub":"b3749862-40a1-7081-f662-9a184a5f5f47","iss":"https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_UYypx8YBX","client_id":"j0ffh763819o72aqs9g3gci2q","origin_jti":"21abc66e-7b8a-44da-ac5c-bfbf7d470c46","event_id":"1578986c-a0ee-4964-8b1e-7f51980fe348","token_use":"access","scope":"aws.cognito.signin.user.admin","auth_time":1725531799,"exp":1725535399,"iat":1725531799,"jti":"711ca52c-b662-40e2-87c8-b6e576b32e5d","username":"b3749862-40a1-7081-f662-9a184a5f5f47"}' | base64
Now replace the payload part (the center part); we can also check it with jwt.io:
Manipulated JWT Token with invalid signature
The signature doesn’t match, but if we use it again to get the orders, we get another user’s order data:
curl --header "Content-Type: application/json" \
--header "Authorization: eyJraWQiOi..." \
--request POST \
--data '{"action":"orders"}' \
https://<replace with your dvsa api endpoint>/dvsa/order | jq
Response:
{
"status": "ok",
"orders": [
{
"order-id": "b421c33a-373a-4fce-9017-fa7a3625c40f",
"date": 1725527311,
"total": 100,
"status": "processed",
"token": "1aOOyaZ67Uio"
}
]
}
If we take a look at the API Gateway, there is no authorization set, the token is handled on the Lambda function side, and it doesn’t verify the signature:
API Gateway, no Authorization
Let’s edit the method request. There is a preconfigured Cognito Authorizer available. Select it from the dropdown and press Save:
API Gateway, adding Authorization
We need to deploy the change to get it into effect. Press the Deploy API button and select the “dvsa” stage:
API Gateway, deploying API
If you try the curl command again, you get a 401 Forbidden response with a body:
{
"message": "Unauthorized"
}
An alternative option would be to handle the token signature validation on the lambda function code.
Token Validation in the Function Code
I recommend validating before the Lambda function in all use cases where you can, per the Single Responsibility Principle described in the first part of the series. You will also save some chargeable computing time. Sometimes, you might need to do it; a good example is when combining Lambda Authorizers with Amazon API Gateway or Amazon AppSync. There are some examples in my Verified Permissions blog series.
Here is a TypeScript code example that uses the aws-jwt-verify library, which makes it a straightforward operation:
import { CognitoJwtVerifier } from "aws-jwt-verify";
const verifier = CognitoJwtVerifier.create({
userPoolId: "<cognito user pool identifier>",
tokenUse: "access",
clientId: "<cognito client identifier>",
});
const token = await verifier.verify(authorizationToken);
The verify function returns the decoded token payload, which you can then use to access the individual claims.
Conclusion
The article demonstrated a critical security vulnerability in the DVSA application related to improper handling of Cognito access tokens. Key takeaways:
- The application initially lacked proper API Gateway authorizers, allowing manipulation of Cognito access tokens.
- By altering the JWT token’s payload, an attacker could access another user’s order data, bypassing authentication.
- The Lambda function handling the token did not verify the signature, leaving the application open to token manipulation attacks.
- Implementing a Cognito Authorizer in the API Gateway addressed the vulnerability, which correctly validates the token signature.
- An alternative solution would be implementing token signature validation within the Lambda function code.