Published on

XS-Leaks (Cross-Site Leaks) Attacks and Prevention

Authors
null

Are you exposing your users to malicious websites? Learn how xs-leaks are used to exfiltrate data from a web application and how to prevent it in 7 steps.

What are XS-leaks?

XS-Leaks (or Cross-Site Leaks) are a set of browser side-channel attacks. They enable malicious websites to infer data from the users of other web applications.

The Twitter silhouette attack was a superb example.

Same Origin Policy

Before we get started, it's helpful to understand SOP (Same Origin Policy), which is the heart and soul of the web browser security model. It is a rule that more or less says:

  1. Two URLs are of the same origin if their protocol, port (if specified), and host are the same.
  2. A website from any origin can freely send GET, POST, HEAD, and OPTIONS requests to any other origin. Furthermore, the request will include the user's cookies (including the session ID) to that origin.
  3. While sending requests is possible, a website from one origin cannot directly read the responses from another origin.
  4. A website can still consume resources from those HTTP responses, such as by executing scripts, using fonts/styles, or displaying images. The JSONP hack takes advantage of this fourth rule (don't use JSONP).
  5. A website can from one origin can have restricted access to a window in another origin, if it gets a handle to the window. Most notably windows from different origins can change each other's URL (anotherWindow.location.replace("https://www.evil.com").

For example, this website's origin is https://www.appsecmonkey.com/ where the protocol is https. The host is www.appsecmonkey.com, and the port is not specified (which is implicit 443 because of the https protocol).

Alrighty, that's the gist of it. Let's get to the attacks.

XS-leaks through timing attacks

Browsers make it easy to time cross-domain requests.

var start = performance.now()

fetch('https://example.com', {
  mode: 'no-cors',
  credentials: 'include',
}).then(() => {
  var time = performance.now() - start
  console.log('The request took %d ms.', time)
})
The request took 129 ms.

This makes it possible for a malicious website to differentiate between responses. Suppose that there is a search API for patients to find their own records. If the patient has diabetes and searches for "diabetes", the server returns data.

GET /api/v1/records/search?query=diabetes

{ 'records': [{ 'id': 1, ... }] }

And if the patient doesn't have diabetes, the API returns an empty JSON.

GET /api/v1/records/search?query=diabetes

{ 'records': [] }

Generally, the former request would take a longer time. An attacker could then create a malicious website that clocks requests to the "diabetes" URL and determines whether or not the user has diabetes.

You can expand the attack and search for a... b... c... d... yes. da.. db... di... yes. This sort of attack is known as XS-search.

See all of the timing attacks on xsleaks.dev.

XS-leaks through error-based attacks

The next side-channel on our list is strategically catching error messages with JavaScript. Suppose that a page returns either 200 OK or 404 not found, depending on some sensitive user data.

An attacker could then create a page like the following, which queries the application and determines whether the endpoint returns an error for the browser user or not.

function checkError(url) {
  let script = document.createElement('script')
  script.src = url
  script.onload = () => console.log(`[+] GET ${url} succeeded.`)
  script.onerror = () => console.log(`[-] GET ${url} returned error.`)
  document.head.appendChild(script)
}

checkError('https://www.example.com/')
checkError('https://www.example.com/this-does-not-exist')
[-] GET https://www.example.com/ succeeded.
[+] GET https://www.example.com/this-does-not-exist returned error.

XS-leaks through frame counting

By obtaining a handle to a frame, it is possible to access the frame's window.length property which is used to retrieve the number of frames (IFRAME or FRAME) in the window.

This knowledge can sometimes have security/privacy implications. For example, a website may render a profile page differently with a varying number of frames based on some user data.

There are a couple of ways to obtain a window handle. The first is to call window.open, which returns the handle.

var win = window.open('https://example.com')
console.log('Waiting 3 seconds for page to load...')
setTimeout(() => {
  console.log('%d FRAME/IFRAME elements detected.', win.length)
}, 3000)

Another is to frame the target website and get the handle of the frame.

<iframe name="framecounter" src="https://www.example.com"></iframe>
<script>
  var win = window.frames.framecounter

  console.log('Waiting 3 seconds for page to load...')
  setTimeout(() => {
    console.log('%d FRAME/IFRAME elements detected.', win.length)
  }, 3000)
</script>

Those two are arguably the most important. There are others, such as window.opener and window.parent. See this article for a more comprehensive list.

Read more about frame counting on xsleaks.dev.

XS-leaks through detecting navigations

Knowing whether or not the browser navigated (e.g., redirected) somewhere, it is often possible to infer data about the user. For example, authenticated portions of websites tend to redirect the user to the login page. Unless, of course, the user is logged in already.

Observing navigations gives malicious websites the power to see which websites the browser user is logged in to, which is a huge privacy concern.

There are multiple ways by which malicious websites can detect redirects. These include:

  • Creating a frame and counting how many times onload is called.
  • Retrieving the history.length from a window handle.
  • Creating a Content Security Policy (CSP) on the malicious website triggers exceptions when specific URL addresses are requested.

See them all here: https://xsleaks.dev/docs/attacks/navigations/

XS-leaks through browser cache

When users visit websites, the resources from those sites are usually cached and stored on the user's disk so they won't have to be downloaded again. This saves bandwidth, lowers server load, and improves user experience.

Unfortunately, the timing- and error-based xsleak variations can take advantage of this and determine whether a user has visited a website before.

The cache timing variation is simple, time the request, and if it's instantaneous, then the resource was cached.

The error-based version is slightly more involved. It's taking advantage of the fact that cached resources are never actually requested from the server. As such, an invalid HTTP request for a cached resource does not raise an exception (because the web server never gets a chance to reject it).

Read about both of them on xsleaks: https://xsleaks.dev/docs/attacks/cache-probing/.

XS-leaks through ID-fields in frames

This one takes advantage of the fact that the https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event event gets fired when a frame with an url like https://www.example.com/#example jumps to the element example.

If there is no example on the page, the event doesn't fire.

Read more about this variation on xsleaks: https://xsleaks.dev/docs/attacks/id-attribute/

XS-leaks through many other things

The variations mentioned thus far should give you the idea, but there are others. You can go to xsleaks.dev for more attacks and details.

How to prevent XS-leaks?

You won't be able to completely prevent all xsleaks in all browsers. The world isn't ready for that yet. But you can be pretty safe, especially for Chrome or Edge, which have all the bleeding edge security features under their belts. Here's how:

  1. Protect your cookies with the SameSite attribute.
  2. Use Content-Security-Policy and X-Frame-Options to prevent framing.
  3. Consider using Cache-Control to disable caching.
  4. Use the fetch metadata headers and the Vary header to prevent cache probes.
  5. Implement a Cross-Origin Opening Policy.
  6. Implement a Cross-Origin Resource Policy.
  7. Implement an isolation policy.

Protect your cookies with the SameSite attribute

All major browsers support a cool feature called SameSite cookies. When you set a cookie with SameSite=Lax, browsers will not include it in cross-origin POST requests, which works nicely against CSRF attacks.

Crucially for our xsleaks use case here, it also blocks GET requests that are not top-level navigation, which is to say, that script-tags, fetch-requests, image tags, etc., will not send the cookie anymore.

Set-Cookie: SessionId=123; ...other options... SameSite=Lax

Use Content-Security-Policy and X-Frame-Options to prevent framing

Another beautiful browser feature is the Content Security Policy, or CSP for short. CSP can be incredibly effective against XSS attacks. But in this case, we are interested in blocking framing, and the CSP recipe for that is:

Content-Security-Policy: frame-ancestors 'none';

This CSP policy will prevent all modern browsers from letting any other website frame your application. If you want to support Internet Explorer as well, then also send X-Frame-Options.

X-Frame-Options: DENY

Consider using Cache-Control to disable caching

Disabling the cache is not for everyone. I, for example, couldn't possibly do it for this blog. But arguably, the most effective way to prevent caching-related xsleaks vectors is to disable caching for your website altogether. You can do this by returning the following Cache-Control header in all your responses:

Cache-Control: no-store, max-age=0

Use the fetch metadata headers and the Vary header to prevent cache probes.

This approach is much more feasible but not supported by all browsers yet. The idea is to Vary the cache based on the fetch metadata request headers.

The Sec-Fetch-Site request header will contain the value cross-site if a malicious website attempts to make requests to your application. And because of the Vary header, that malicious website will sort-of have a cache of its own. It will not be able to deduce what the browser user has cached on the website.

Vary: Sec-Fetch-Site

Implement a Cross-Origin Opener Policy

The Cross-Origin-Opener-Policy is an HTTP response header that restricts malicious websites from obtaining a window handle to your website. You can set it like so:

Cross-Origin-Opener-Policy: same-origin

It is already fully supported in Chrome, Edge, and Firefox.

Implement a Cross-Origin Resource Policy

The Cross-Origin-Resource-Policy is an HTTP response header that restricts malicious websites from reading/embedding/rendering resources from your domain. Read more about it here. You can set it like so:

Cross-Origin-Resource-Policy: same-origin

Implement an isolation policy

Browsers have started to implement a relatively new security feature called fetch metadata request headers. They can be used on the server-side to block or allow requests based on the context in which they happened.

This kind of middleware that blocks requests based on the fetch metadata headers is called an isolation policy.

Read more about the fetch metadata headers and isolation policies here.

Conclusion

There are quite a few XS-leaks, and browser vendors are coming up with tools to beat them as we speak.

Preventing all of them is not easy, if even possible. But by following the guidelines in this article and on https://xsleaks.dev/, you should be fine.