Skip to content

Browser Side Channels

Eduardo' Vela" Nava (sirdarckcat) edited this page Feb 9, 2019 · 29 revisions

Well-known DOM APIs

There are a few well known DOM APIs that leak cross origin information.

Frame Count

The Window DOM API documents how to traverse across cross origin windows (under other browsing contexts). One of these is the number of frames in the document (window.length).

let win /*Any Window reference, either iframes, opener, or open()*/;
win.frames.length;

History Length

The History DOM API documents that the history object can know how many entries there are in the history of the user. This leak can be used to detect when a cross-origin page had some types of navigations (eg, those via history.pushState or just normal navigations).

Note that for detecting navigations on pages that can be iframed, it is possible to just count how many times the onload event was triggered (see Frame timing), in cases when the page can't be inside a frame, then this mechanism can be useful.

history.length; // leaks if there was a javascript/meta-refresh redirect

Error Events

For most HTML elements that load subresources have error events that are triggered in the case of a response error (eg, error 500, 404, etc) as well as parsing errors.

Media Size

Images, Videos, Audio and a few other resources allow for measuring their duration (in the case for video and audio) and size (for images).

Timing

For timing we have to consider two factors:

  1. A consequence to observe in another window/origin (eg, network, javascript, etc).
  2. A mechanism to measure the time that passed.

To defend against these attacks, browsers try to limit the amount of information leaked across windows/origins, and in some cases, also try to limit the accuracy of different mechanisms for measuring time.

Measuring time

The most common used mechanisms for measuring time are:

  1. performance.now()
  2. SharedArrayBuffer

Request timing

This type of measurement can be mitigated with same-site cookies in strict mode (for GET requests), or in lax mode (for POST requests). Using same-site cookies in lax mode is not safe, as it can be bypassed by timing navigation requests.

let before = performance.now()
await fetch("//mail.com/search?q=foo")
let request_time = performance.now() - before

Cross-document request timing

In chrome the number of HTTP requests made by another window/document can be calculated using the network pool. To do this, the attacker needs two windows/documents.

Window A:

  • Wait for a click to open window B

Window B:

  • Exhaust all sockets except one by performing 255 fetch operations to different domains. The webserver will sleep for 30 seconds before replying to the request.
  • Redirect window.opener to the target url that we want to time
  • fetch('//attacker.com') in a loop and time how long the request took

Navigation requests

These techniques are used for measuring the time it takes a navigation request to load.

This is useful for measuring the time it takes a GET request to load if protected by same-site cookies in lax mode. This can be mitigated with same-site cookies in strict mode.

Frame timing

This mechanism waits until all subresources finish loading. Note that in pages that set the X-Frame-Options header, this mechanism can only be used for measuring the network request, because subresources are not measured. Note that the difference between onerror and onload is often also important.

<iframe name=f id=g></iframe>
<script>
h = performance.now();
f.location = '//mail.com/search?q=foo';
g.onerror = g.onload = ()=>{
    console.log('time was', performance.now()-h)
};
</script>

Cross-window timing

This mechanism is only useful when a page uses X-Frame-Options and one is interested on the subresources being loaded, or in the javascript code executing for other attacks (such as establishing the starting time for cross-document request timing or multi-threaded JavaScript).

To protect against this types of attacks one might be able to use Cross-Origin-Opener-Policy in the future.

let w=0, z=0, v=performance.now();
onmessage=()=>{
  try{
    if(w && w.document.cookie){
      // still same origin
    }
    postMessage('','*');
  }catch(e){
    z=performance.now();
    console.log('time to load was', v-z);
  }
};
postMessage('','*');
w=open('//www.google.com/robots.txt');

JavaScript Execution

Measuring JavaScript execution can be useful for understanding when certain events are triggered, and how long some operations take.

Examples:

Single-threaded JavaScript

In browsers other than Chrome, all JavaScript code (even cross-origin) runs in the same thread, which means that one can measure for how long code runs in another origin by measuring how long it takes for code to run next in the event pool.

Multi-threaded JavaScript

In Chrome, every site runs in a different process, and every process has their own thread, which means that in order to measure the timing of JavaScript execution in another thread, we have to measure it in a different way. One way to do this is by:

  1. Register a service worker on the attacker's origin.
  2. Opening the target window, and detect when the document is loaded (using cross window timing)
  3. In an interval attempt to navigate the window away in the event loop to a page that will be caught by the service worker.
  4. When the request is received, remember the current time, and return with a 204 response.
  5. Measure how long it took for the navigation to be requested, to the request to the service worker to arrive.

Size

Some times considered a vulnerability by browsers, and some times measured with timing. Regardless, it is some times possible to (incidentally) defend against this types of attacks by using CORB, and CORP. As their implementation also breaks some of the APIs.

Examples:

Flash

Current public mechanism to learn size of cross-site requests is with Flash.

Cache Quota API

By abusing the Cache API and the quota a single origin receives, it's possible to measure the size of a single response. To protect against this attack browsers add random noise to the quota calculation.

  1. Firefox adds a random number up to 100K and reduces accuracy to the closest 20K (code).
  2. Chrome adds a random number up to 14,431K (code).

One can still perform the attack with the noise added, although it requires a lot more requests.

Cache Timing

By abusing the Cache API, and the browser's cache, one can measure how long it takes for a simple request to be loaded from the different levels of caching. Assuming a longer response will take longer to load. By abusing techniques (such as "inflating" the response size), one can make the difference through timing more measurable.

XSS Auditor

If a given page has any scripts, and if the XSS auditor can be detected, then one can figure out the presence of a specific script, or form. See here for more details.

Clone this wiki locally