Search code examples
javascriptfacebookgreasemonkeyuserscriptstampermonkey

Why does changing grant from none to GM_xmlhttpRequest break my code?


In short instead of alerting URLs and the response body I'd like to send it to my app. This code works but I can not use GM_xmlhttpRequest unless I grant it.

Changing nothing else the code magically breaks. I'm unsure what is changed and how to fix it. I thought I could use console.log and copy/paste the data into my app however Facebook disables console.log.

I thought about doing xmlhttpRequest but that too is somehow blocked. I tested by executing code in a console. The 3 lines seem to work everywhere except on a Facebook domain. I believe it has something to do with CORS.

// ==UserScript==
// @name        FBTest
// @namespace   test
// @include     https://*.facebook.com/*
// @version     1
// @grant       none
// ==/UserScript==
//change none to GM_xmlhttpRequest
(function() {
    var proxied = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function(method, url) {
        alert(url);
        return proxied.apply(this, [].slice.call(arguments));
    }; 
})();

Solution

  • When you grant GM_xmlhttpRequest, it switches on the sandbox -- which means that you cannot access window.XMLHttpRequest like that as it is in a different context now.

    To work around this, use script injection to intercept the AJAX. And, use either messaging or a custom event to access the data in your userscript's context.

    Here's an example script using a custom event (less vulnerable to 3rd-party exploits):

    // ==UserScript==
    // @name        _Intercept AJAX with grant/sandbox on
    // @match       https://*.facebook.com/*
    // @grant       GM_xmlhttpRequest
    // ==/UserScript==
    
    function xmlOpenIntercept () {
        var proxied = window.XMLHttpRequest.prototype.open;
        window.XMLHttpRequest.prototype.open = function (method, newUrl) {
            var cEvnt = new CustomEvent ('newAjaxStart', {'detail': newUrl} );
            document.body.dispatchEvent (cEvnt);
    
            return proxied.apply (this, [].slice.call (arguments) );
        };
    }
    addJS_Node (null, null, xmlOpenIntercept);  //-- Injects code
    
    
    //--- This code listens for the right kind of message.
    document.body.addEventListener ("newAjaxStart", receiveAjaxMessage);
    
    function receiveAjaxMessage (zEvent) {
        console.log ("Intercepted AJAX to: ", zEvent.detail);
    }
    
    function addJS_Node (text, s_URL, funcToRun, runOnLoad) {
        var D                                   = document;
        var scriptNode                          = D.createElement ('script');
        if (runOnLoad)  scriptNode.addEventListener ("load", runOnLoad);
        scriptNode.type                         = "text/javascript";
        if (text)       scriptNode.textContent  = text;
        if (s_URL)      scriptNode.src          = s_URL;
        if (funcToRun)  scriptNode.textContent  = '(' + funcToRun.toString() + ')()';
    
        var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
        targ.appendChild (scriptNode);
    }