Skip to main content
All Articles

API Gateway Authorizers: Vulnerable By Design (Be Careful!)


· 5 min read

I had the benefit of joining the AWS Community Day in Zürich this week, most went as expected but, then an interesting question came up...

Does caching in API Gateway create vulnerabilities for products using Authorizer Caching?

Authorization

When your users call your API, you have an obvious need to verify these requests should actually be allowed. I've talked extensively about this in my academy article on what the @#!? is Auth.

Even if you haven't read that article, if you are well versed in the need for users to authenticate and authorize to your specific service API and endpoints, then you get the gist.

So you have a need to verify the access tokens sent by users on ever request. When using AWS this means using API Gateway, and when using API Gateway that likely means you'll be using an API Gateway Authorizer.

Authorizers in API exist so that you can verify more easily verify the user access tokens. As a reminder an authorization token looks like this:

Decoded JWT authorization token.
{
"identityProviderId": "https://authress.io",
"userId": "TechInternals|test-user-001",
"expires": 1761483600,
"signatureKeyId": "example-key",
"signature": "SflKxwRJSMeKKF2Qt4fwpMe"
}

And the process to verify the token looks like this:

Verifying user tokens
const authressClient = new AuthressClient({ authressApiUrl });
const userIdentity =
await authressClient.verifyToken(userToken);

Of course swapping in your favorite open source JWT verifier. More extensive details on this depending on your identity provider are available

Now I know what you are thinking:

I'm going to get a lot of requests from the same user to my same API, for different resources. That means they are all going to have the same JWT. Wouldn't it be great to cache those results so that I don't need to verify the same JWT over and over again every time this same user makes a similar request for similar data with same JWT.

And you would be right!

Caching

However, if you wrote the above code and you cache it, you might start to see a problem with it...

Caching by default in API gateway is keyed from the authorization token only and nothing else. This means that the result from one request will interfere with the next one.

Let's take for example the policy result from an AWS API Gateway Authorizer. It might see something like this:

API Gateway Authorizer policy result containing Resource.
const policy = {
principalId: userIdentity.sub,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'execute-api:Invoke',
Resource: event.methodArn
}]
},
context: {
principalId: userIdentity.sub
}
};

There is actually a problem with this however. The cache key by default is only the JWT, but the result of this policy says that the user is only allowed to one particular event.methodArn. A method ARN as a reminder is like GET /orders/order_id_123.

That means on a followup request with the same JWT to a different endpoint GET /orders/order_id_456, even if the user should have access to that resource and their JWT is still valid, API Gateway will deny that request.

Why?

Well that is simple, because the result is cached based only on the JWT. The cached result specifies only that one route GET /orders/order_id_123 has been authorized.

Worst case scenario, you have a short cache time, and the only thing that happens is a short but confusing user experience, that quickly results in the correct behavior.

But you are smart, you realize there is a fix, instead of passing the event.methodArn as the result policy you specify ['arn:aws:execute-api:*:*:*'] as the resource result.

Now subsequent requests as long as the JWT is still valid, irrespective of the endpoint, will allow the user through!

🎉🎉🎉

And this works great.

But you are thinking why stop there. Can we go further?

And the answer is also yes.

Authorization of Granular Resources Based Access Control

You might be using solutions such as AWS Verified Permissions hoping to connect it together with Cognito and API Gateway.

Now I know what you are thinking, why is Warren investigating verified permissions when Authress already solves all these problems? Well sometimes even I have to write an article about how the integration of default resources in AWS can cause security misconfigurations.

Your decision is—Not just cache the validity of the JWT, but you also want to cache whether or not the user actually has access to call the endpoint in question. That is, you decide to take the additional step of verifying the user's authorization and you also cache it, then you will have just created a majority security vulnerability in your application.

Do you already see what the problem might be?

In your authorizer you are likely to write:

The authorization check
const hasAccess = await authress.userPermissions.authorizeUser(
userId,
'resource',
'resource:read');

If you are checking the user's access inside the authorizer and it is cached, then subsequent requests to the same API will utilize the cached result.

If the user has:

  • Access to orders_123
  • No Access to orders_456

And then calls

  1. GET orders_123
  2. GET orders_456

They will incorrectly be allowed to access that second order.

That's because the authorizer will have access ALLOW for:

Authorization check for orders_123
const hasAccess = await authress.userPermissions.authorizeUser(
userId,
'orders_123',
'orders:read');

That ALLOW is set as the cache result for the user's JWT:

Stored Cached values by default
JWT_001 => ALLOW

The cache doesn't contain the orderId in it. Or said differently the cache is NOT:

Stored Cached values with additional cache keys
[JWT_001, GET, orders_123] => ALLOW

That means when the second request comes in, we got to the cache table, see the cache already exists for JWT_001, return ALLOW, and never actually check the authorization for orders_456.

Removing the security vulnerability

It would be nice of API Gateway to be secure by default and require the identity source cache key to include the resource path and method. But it isn't, so it doesn't. And this risk is similar to ones experienced by engineers all day long with caching in CloudFront. And if we think about the frequency of issues with caching in CloudFront which has no security vulnerability, we can realize that—since AWS created the Verified Permissions service and related functionality, this opened a huge security vulnerability potential configuration in API Gateway.

This isn't an explicit vulnerability in the service though, since the vulnerability only exists based on improper configuration, but here the improper configuration is the default. Show me a company using API Gateway and AWS Verified Permissions, and I bet I can show you a Security Bounty waiting to be collected.

The resolution here is to force the API Gateway Authorizer to cache also on the httpMethod (Context) and path (Context).

API Gateway Authorizer expected configuration

Once that is done, now API Gateway will close this security hole because the cache key will match the authorization check performed by your authorization provider.

Recommendations

On your side there is little you can do to remove the pit of failure. Review documentation, invest in deep understanding of the tools you used especially when security is involved. I guess also keep reading my posts as I often try to focus on security related topics.

On the AWS side, there is absolutely a strategy that would have fixed this by design. The authorizer should not have access to the Path and Method properties of the HTTP request unless the identity source cache key includes them. This would require breaking existing configurations, but it would be in the name of security by default.

Going further

There are actually lots of different ways to cache permissions results in AWS when not even using Verified Permissions and for an extensive list of the options and my personal recommendations check out this Auth Academy article on the topic.

info

Want to chat more about this topic? Join our community!