1"use strict";(self.webpackChunkAuthressKnowledgeBase=self.webpackChunkAuthressKnowledgeBase||[]).push([[2660],{3905:(e,t,a)=>{a.d(t,{Zo:()=>h,kt:()=>p});var n=a(67294);function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function i(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function o(e){for(var t=1;t<arguments.length;t++){var a=null!=arguments[t]?arguments[t]:{};t%2?i(Object(a),!0).forEach((function(t){r(e,t,a[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(a)):i(Object(a)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(a,t))}))}return e}function s(e,t){if(null==e)return{};var a,n,r=function(e,t){if(null==e)return{};var a,n,r={},i=Object.keys(e);for(n=0;n<i.length;n++)a=i[n],t.indexOf(a)>=0||(r[a]=e[a]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n<i.length;n++)a=i[n],t.indexOf(a)>=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var l=n.createContext({}),u=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},h=function(e){var t=u(e.components);return n.createElement(l.Provider,{value:t},e.children)},c={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,h=s(e,["components","mdxType","originalType","parentName"]),d=u(a),p=r,y=d["".concat(l,".").concat(p)]||d[p]||c[p
1]||i;return a?n.createElement(y,o(o({ref:t},h),{},{components:a})):n.createElement(y,o({ref:t},h))}));function p(e,t){var a=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=a.length,o=new Array(i);o[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s.mdxType="string"==typeof e?e:r,o[1]=s;for(var u=2;u<i;u++)o[u]=a[u];return n.createElement.apply(null,o)}return n.createElement.apply(null,a)}d.displayName="MDXCreateElement"},6856:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>o,default:()=>c,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var n=a(87462),r=(a(67294),a(3905));const i={hide_table_of_contents:!1,title:"API Gateway Authorizers: Vulnerable By Design (Be Careful!)",authors:"warren-parad",description:"API Gateway authorizers provide a unique opportunity to exposure yourself to malicious requests when defaults are used, and even when the defaults aren't used.",image:"./index.jpg",image_alt:"A whole in the AWS Cloud created by a giant explosion of where API Gateway used to be."},o=void 0,s={permalink:"/knowledge-base/articles/2025/05/25/api-gateway-authorizers-vulnerable-by-design",source:"@site/articles/2025-05-25-api-gateway-authorizers-vulnerable-by-design/index.md",title:"API Gateway Authorizers: Vulnerable By Design (Be Careful!)",description:"API Gateway authorizers provide a unique opportunity to exposure yourself to malicious requests when defaults are used, and even when the defaults aren't used.",date:"2025-05-25T00:00:00.000Z",formattedDate:"May 25, 2025",tags:[],readingTime:4.3966666666666665,hasTruncateMarker:!1,authors:[{name:"Warren Parad",url:"https://warrenparad.net",page:!0,socials:{bluesky:"wparad.bsky.social",github:"wparad",linkedin:"warren-parad"},imageURL:"https://authress.io/knowledge-base/img/authors/warren-parad.jpg",key:"warren-parad"}],frontMatter:{hide_table_of_contents:!1,title:"API Gateway Authorizers: Vulnerable By Design (Be Careful!)",authors:"warren-parad",description:"API Gateway authorizers provide a unique opportunity to exposure yourself to malicious requests when defaults are used, and even when the defaults aren't used.",image:"./index.jpg",image_alt:"A whole in the AWS Cloud created by a giant explosion of where API Gateway used to be."},prevItem:{title:"What the @#!? is Auth",permalink:"/knowledge-base/articles/2025/05/26/what-the-heck-is-auth"},nextItem:{title:"Meeting Impossible SLAs: How we made our uptime 99.999%",permalink:"/knowledge-base/articles/2025/03/18/meeting-impossible-slas"}},l={image:a(40007).Z,authorsImageUrls:[void 0]},u=[{value:"Authorization",id:"authorization",level:2},{value:"Caching",id:"caching",level:2},{value:"Authorization of Granular Resources Based Access Control",id:"authorization-of-granular-resources-based-access-control",level:2},{value:"Removing the security vulnerability",id:"removing-the-security-vulnerability",level:2},{value:"Recommendations",id:"recommendations",level:2},{value:"Going further",id:"going-further",level:2}],h={toc:u};function c(e){let{components:t,...i}=e;return(0,r.kt)("wrapper",(0,n.Z)({},h,i,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"I had the benefit of joining the ",(0,r.kt)("a",{parentName:"p",href:"https://www.awsug.ch/"},"AWS Community Day in Z\xfcrich")," this week, most went as expected but, then an interesting question came up..."),(0,r.kt)("blockquote",null,(0,r.kt)("p",{parentName:"blockquote"},"Does caching in API Gateway create vulnerabilities for products using Authorizer Caching?")),(0,r.kt)("h2",{id:"authorization"},"Authorization"),(0,r.kt)("p",null,"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 ",(0,r.kt)("a",{parentName:"p",href:"/academy/topics/implementating-user-login"},"what the @#!? is Auth"),"."),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"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:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="Decoded JWT authorization token."',title:'"Decoded',JWT:!0,authorization:!0,'token."':!0}
1,'{\n "identityProviderId": "https://authress.io",\n "userId": "TechInternals|test-user-001",\n "expires": 1761483600,\n "signatureKeyId": "example-key",\n "signature": "SflKxwRJSMeKKF2Qt4fwpMe"\n}\n')),(0,r.kt)("p",null,"And the process to verify the token looks like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="Verifying user tokens"',title:'"Verifying',user:!0,'tokens"':!0},"const authressClient = new AuthressClient({ authressApiUrl });\nconst userIdentity =\n await authressClient.verifyToken(userToken);\n")),(0,r.kt)("p",null,"Of course swapping in your favorite open source JWT verifier. ",(0,r.kt)("a",{parentName:"p",href:"/docs/authentication/validating-jwts"},"More extensive details on this depending on your identity provider are available")),(0,r.kt)("p",null,"Now I know what you are thinking:"),(0,r.kt)("blockquote",null,(0,r.kt)("p",{parentName:"blockquote"},"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.")),(0,r.kt)("p",null,"And you would be right!"),(0,r.kt)("h2",{id:"caching"},"Caching"),(0,r.kt)("p",null,"However, if you wrote the above code and you cache it, you might start to see a problem with it..."),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"Let's take for example the policy result from an AWS API Gateway Authorizer. It might see something like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="API Gateway Authorizer policy result containing Resource."',title:'"API',Gateway:!0,Authorizer:!0,policy:!0,result:!0,containing:!0,'Resource."':!0},"const policy = {\n principalId: userIdentity.sub,\n policyDocument: {\n Version: '2012-10-17',\n Statement: [{\n Effe
1ct: 'Allow',\n Action: 'execute-api:Invoke',\n Resource: event.methodArn\n }]\n },\n context: {\n principalId: userIdentity.sub\n }\n };\n")),(0,r.kt)("p",null,"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 ",(0,r.kt)("inlineCode",{parentName:"p"},"event.methodArn"),". A method ARN as a reminder is like ",(0,r.kt)("inlineCode",{parentName:"p"},"GET /orders/order_id_123"),"."),(0,r.kt)("p",null,"That means on a followup request with the same JWT to a different endpoint ",(0,r.kt)("inlineCode",{parentName:"p"},"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."),(0,r.kt)("p",null,(0,r.kt)("strong",{parentName:"p"},"Why?")),(0,r.kt)("p",null,"Well that is simple, because the result is cached based only on the JWT. The cached result specifies only that one route ",(0,r.kt)("inlineCode",{parentName:"p"},"GET /orders/order_id_123")," has been authorized."),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"But you are smart, you realize there is a fix, instead of passing the ",(0,r.kt)("inlineCode",{parentName:"p"},"event.methodArn")," as the result policy you specify ",(0,r.kt)("inlineCode",{parentName:"p"},"['arn:aws:execute-api:*:*:*']")," as the resource result."),(0,r.kt)("p",null,"Now subsequent requests as long as the JWT is still valid, irrespective of the endpoint, will allow the user through!"),(0,r.kt)("p",null,(0,r.kt)("strong",{parentName:"p"},"\ud83c\udf89\ud83c\udf89\ud83c\udf89")),(0,r.kt)("p",null,"And this works great."),(0,r.kt)("p",null,"But you are thinking why stop there. Can we go further?"),(0,r.kt)("p",null,"And the answer is also yes."),(0,r.kt)("h2",{id:"authorization-of-granular-resources-based-access-control"},"Authorization of Granular Resources Based Access Control"),(0,r.kt)("p",null,"You might be using solutions such as ",(0,r.kt)("a",{parentName:"p",href:"https://aws.amazon.com/verified-permissions/"},"AWS Verified Permissions")," hoping to connect it together with Cognito and API Gateway."),(0,r.kt)("p",null,"Now I know what you are thinking, why is Warren investigating verified permissions when ",(0,r.kt)("a",{parentName:"p",href:"https://authress.io"},"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."),(0,r.kt)("p",null,"Your decision is\u2014Not 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."),(0,r.kt)("p",null,"Do you already see what the problem might be?"),(0,r.kt)("p",null,"In your authorizer you are likely to write:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="The authorization check"',title:'"The',authorization:!0,'check"':!0},"const hasAccess = await authress.userPermissions.authorizeUser(\n userId,\n 'resource',\n 'resource:read');\n")),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"If the user has:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Access to ",(0,r.kt)("inlineCode",{parentName:"li"},"orders_123")),(0,r.kt)("li",{parentName:"ul"},"No Access to ",(0,r.kt)("inlineCode",{parentName:"li"},"orders_456"))),(0,r.kt)("p",null,"And then calls"),(0,r.kt)("ol",null,(0,r.kt)("li",{parentName:"ol"},"GET ",(0,r.kt)("inlineCode",{parentName:"li"},"orders_123")),(0,r.kt)("li",{parentName:"ol"},"GET ",(0,r.kt)("inlineCode",{parentName:"li"},"orders_456"))),(0,r.kt)("p",null,"They will incorrectly be allowed to access that second order."),(0,r.kt)("p",null,"That's because the authorizer will have access ALLOW for:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="Authorization check for orders_123"',title:'"Authorization',check:!0,for:!0,'orders_123"':!0},"const hasAccess = await authress.userPermissions.authorizeUser(\n userId,\n 'orders_123',\n 'orders:read');\n")),(0,r.kt)("p",null,"That ",(0,r.kt)("inlineCode",{parentName:"p"},"ALLOW")," is set as the cache result for the user's JWT:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="Stored Cached values by default"',title:'"Stored',Cac
1hed:!0,values:!0,by:!0,'default"':!0},"JWT_001 => ALLOW\n")),(0,r.kt)("p",null,"The cache doesn't contain the orderId in it. Or said differently the cache is ",(0,r.kt)("strong",{parentName:"p"},"NOT"),":"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="Stored Cached values with additional cache keys"',title:'"Stored',Cached:!0,values:!0,with:!0,additional:!0,cache:!0,'keys"':!0},"[JWT_001, GET, orders_123] => ALLOW\n")),(0,r.kt)("p",null,"That means when the second request comes in, we got to the cache table, see the cache already exists for ",(0,r.kt)("inlineCode",{parentName:"p"},"JWT_001"),", return ",(0,r.kt)("inlineCode",{parentName:"p"},"ALLOW"),", and never actually check the authorization for ",(0,r.kt)("inlineCode",{parentName:"p"},"orders_456"),"."),(0,r.kt)("h2",{id:"removing-the-security-vulnerability"},"Removing the security vulnerability"),(0,r.kt)("p",null,"It would be nice of API Gateway to be secure by default and require the ",(0,r.kt)("inlineCode",{parentName:"p"},"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\u2014since AWS created the Verified Permissions service and related functionality, this opened a huge security vulnerability potential configuration in API Gateway."),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"The resolution here is to force the API Gateway Authorizer to cache also on the ",(0,r.kt)("inlineCode",{parentName:"p"},"httpMethod (Context)")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"path (Context)"),"."),(0,r.kt)("div",{className:"image-md"},(0,r.kt)("p",null,(0,r.kt)("img",{alt:"API Gateway Authorizer expected configuration",src:a(87980).Z,width:"1008",height:"1595"}))),(0,r.kt)("p",null,"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."),(0,r.kt)("h2",{id:"recommendations"},"Recommendations"),(0,r.kt)("p",null,"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."),(0,r.kt)("p",null,"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."),(0,r.kt)("h2",{id:"going-further"},"Going further"),(0,r.kt)("p",null,"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 ",(0,r.kt)("a",{parentName:"p",href:"/docs/advanced/caching"},"Auth Academy article")," on the topic."),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"Want to chat more about this topic? ",(0,r.kt)("a",{parentName:"p",href:"https://authress.io/community"},"Join our community"),"!")))}c.isMDXComponent=!0},87980:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/api-gateway-configuration-9dcc2fd413af6310621216402109a47d.png"},40007:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/index-e1652921e70283bf3a1fba69a8c4129c.jpg"}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.