Account Takeover in Canvas Apps served in Comet due to failure in Cross-Window-Message Origin validation (62500)

We discussed this vulnerability during Episode 185 on 06 February 2023

A rather simple bug in validating the origin of a Cross-window message due to inappropriately handling null values.

First, lets talk about the normal flow of things. Some Facebook system embeds third-party applications into its pages. those third-parties can send messages to their parent frame (the Facebook page) to do the OAuth flow for the user. That parent page, has an iframe to the “Compat” endpoint which it can communicate with. It is the “Compat” page which actually triggers the OAuth flow, and dialog display, it uses the arbiter endpoint for its redirect_uri. This arbiter endpoint will send the access code back to the “Compat” page, which sends it to whatever Facebook page asked for it, who should do any necessary security checks (also doing this before they send the OAuth request to “Compat”).

With the arbiter if someone could get the arbiter to return the access code to them without actually being from the “Compat” page or some other trusted location, that would be a vulnerability. Similarly, the “Compat” page only trusts pages on the same origin and sends back any access codes it recieves without any security. So if one could embed the “Compat” page and pass the checks it performs before accepting communication, they could get an OAuth flow triggered for a first-party application and recieve the access token back.

This is where the core issue lies, the “Compat” page attempts to get a URI object for the domain that embeded it, and then compares that with the whitelist. In the following code this is the uri object for the attacker’s location, and a is the whitelisted URI.:

if (this.getProtocol() && this.getProtocol() !== a.getProtocol())
return !1;
if (this.getDomain() && this.getDomain() !== a.getDomain())
return !1;
if (this.getPort() && this.getPort() !== a.getPort())
return !1;
return this.toString() === "" || a.toString() === "" ? !1 : !0

At a glance this code makes sense, but if “Compat” is loaded inside of a sandboxed iframe then the URI object for the attacker’s location will be filled with null values. The way all of these checks are written, null will be treated as a success. this.getProtocol() && this.getProtocol() !== a.getProtocol() for example, never reaches the check with the value in a since it will stop when this.getProtocol() resolves to a falsey value.

There is the apparent null check at the end, but the .toString() on the URI object doesn’t result in “” but “null”, again passing the check.

So any attacker can embed the “Compat” frame inside of a sandboxed iframe, and then trigger the Oauth flow pretending to be a first-party application, and receive back the access tokens.

The author then goes on to details more about the actual exploit chain and some other challenges to get around, but I felt that this was atleast the interesting and more generic part of the writeup to summarize but give the rest a read if you’re interesting in some Facebook/Meta specific details.