Navigations
October 1, 2020
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 theonload
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 byhistory.pushState
or by regular navigations. To get the value ofhistory.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. Run demo
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 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. Run demo
In the snippet below , we’ve added a sandboxed iframe with downloads disabled to prevent a download modal from appearing.
// 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');
// Don't actually download the file to be stealthy
iframe.sandbox = 'allow-scripts allow-same-origin allow-popups';
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 aniframe
caused by a download attempt, theiframe
does not trigger anonload
event directly. For this reason, in the example above, an outeriframe
was used instead, which listens for anonload
event which triggers when subresources finish loading, includingiframe
s.
important
This attack works regardless of any Framing Protections, because theX-Frame-Options
andContent-Security-Policy
headers are ignored ifContent-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. In the snippet below, we’ve added a sandboxed iframe with disabled downloads to prevent a download modal from appearing.
// Set the destination URL
var url = 'https://example.org';
// Don't actually download the file to be stealthy
var iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts allow-same-origin allow-popups';
document.body.appendChild(iframe);
// Get a window reference
var win = iframe.contentWindow.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 #
Max redirects #
When a page initiates a chain of 3XX redirects, browsers limit the maximum number of redirects to 20 1. This can be used to detect the exact number of redirects occurred for a cross-origin page by following the below approach 2:
- As a malicious website, initiate 19 redirects and make the final 20th redirect to the attacked page.
- If the browser threw a network error, at least one redirect occurred. Repeat the process with 18 redirects.
- If the browser didn’t threw a network error, the number of redirects is known as
20 - issued_redirects
.
To detect an error one can use Error Events
If performed in a top window, this also works with SameSite lax cookies and other cross-site protections, such as Framing Isolation Policy or Resource Isolation Policy. Run demo
Inflation (Server-Side Errors) #
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.
Inflation (Client-Side Errors) #
Most browsers have a maximum permitted URL length, above which the navigation will be aborted. For example, Chrome limits URLs to a maximum length of 2MB 3. When this limit is exceeded, the browser may exhibit behavior that can be detected from a cross-origin page. The exact URL length limit and oracle behavior depends on the browser.
When requesting a URL with a fragment, the fragment is preserved on server redirects. For example, if //example.org
redirects to //example.org/redirected
, the browser will navigate to //example.org/redirected#fragment
when //example.org#fragment
is requested. This allows an attacker to artificially inflate the URL length by adding a large fragment to the URL so that it is exactly one character less than the maximum permitted length.
example
Chrome will navigate to anabout:blank
page if the URL length is exceeded. An attacker can detect whether a redirect occurred by checking if the page is still on the same origin.
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 4 5. Run demo
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
:
|
|
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
:
|
|
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. 6
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");
info
Partitioned HTTP Cache Bypass can be prevented using the headerVary: Sec-Fetch-Site
as that splits the cache by its initiator, see Cache Protections. It works because the attack only applies for the resources from the same site, henceSec-Fetch-Site
header will becross-site
for the attacker compared tosame-site
orsame-origin
for the website.
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 (Server-Side Errors) | ✔️ | ❌ | ❌ | RIP |
Inflation (Client-Side Errors) | ❌ | ❌ | ❌ | NIP |
CSP Violations | ❌ \(^{2}\) | ❌ | ❌ | RIP 🔗 NIP |
🔗 – Defense mechanisms must be combined to be effective against different scenarios.
- 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. - 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, in contrast to Strict mode.
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 7.