Search code examples
javascripthttprequestprototype-chain

How can a function reach an external script with [[FunctionLocation]]?


I have some code that is using the google api to show a share dialog on the screen. (this one: https://developers.google.com/drive/api/v3/share-button)

   share_client = new gapi.drive.share.ShareClient()
   share_client.showSettingsDialog()

But after calling showSettingsDialog, it fails with a 403. I can see the request in my network tab: failed request

I know why it fails and I believe that if only I could add an extra parameter to this request, I could fix this issue. The problem is I can't use something like a Service Worker to intercept and change the request, because this request is actually initiated from a script from apis.google.com, and not from my page's location, so I don't have access to it with a Service Worker. It's also very hard, if at all possible, to modify this api code to make the request that I want. I have tried, a lot.

And if I inspect that share_client object, I can see the point where it goes away from the code that I have control over, to an external resource. It's because the function being called has this [[FunctionLocation]], which points to: https://apis.google.com/_/scs/apps-static/_/js/k=oz.gapi. ... /cb=gapi.loaded_2. If I step in the debugger I can see it jumping from this function call, to my local copy of the API, to external scripts hosted in apis.google.com

enter image description here

So my question is how exactly does this work? How can a function point to an external script? Where does this [[FunctionLocation]] comes from? How is it generated? Is it possible to change it? How can it reach this external code without actually making an http request?

Basically I'm asking for any insight on how this all fits together. I'm not so confident that it is actually possible add a parameter to that request like I wanted to, but at this point I'm mostly just curious to learn more about it.


Solution

  • When doing some reverse engineering, we can see this inside the https://apis.google.com/js/api.js JavaScript code:

    var W = decodeURI("%73cript");
    

    Which is equivalent of:

    var W = "script";
    

    The reason is probably to provide some kind of obfuscation to prevent us from reverse engineering the code, but it's not enough to stop us!

    Then, there is this function:

    va = function (a) {
        var b = v.createElement(W);
        b.setAttribute("src", Y ? Y.createScriptURL(a) : a);
        a = ua();
        null !== a && b.setAttribute("nonce", a);
        b.async = "true";
        (a = v.getElementsByTagName(W)[0]) ? a.parentNode.insertBefore(b, a) : (v.head || v.body || v.documentElement).appendChild(b)
    }
    

    Which is a very common method to dynamically add a script to the page (create a script element with a given src, then add it to the DOM).

    This is how the https://apis.google.com/_/scs/apps-static/_/js/k=oz.gapi.fr.VPdiypayOnU.O/m=drive_share/rt=j/sv=1/d=1/ed=1/am=wQc/rs=AGLTcCPUjzWofrE8wWhx40v1IW1jnz4lrQ/cb=gapi.loaded_0 script is being loaded.

    At the end of this script, we can see this:

    lU.prototype.E2 = function() {
        this.Ds.Gh("settings")
    }
    ;
    _.z("gapi.drive.share.ShareClient", lU);
    lU.prototype.setItemIds = lU.prototype.HE;
    lU.prototype.setOAuthToken = lU.prototype.KE;
    lU.prototype.showSettingsDialog = lU.prototype.E2;
    

    This is where the showSettingsDialog is being assigned to ShareClient.prototype. You could easily replace it with your own function but that wouldn't help you since the function you want to modify is some other function (called indirectly by this.Ds.Gh("settings")).

    Unfortunately, I strongly doubt you can modify that underlying function because everything is enclosed in a callback: gapi.loaded_0(function(_) {..., and the only thing made available from outside this callback is ShareClient thanks to this code:

    _.q = this || self; // contains window during execution
    _.z = function(a, b) {
        a = a.split(".");
        var c = _.q;
        a[0]in c || "undefined" == typeof c.execScript || c.execScript("var " + a[0]);
        for (var d; a.length && (d = a.shift()); )
            a.length || void 0 === b ? c = c[d] && c[d] !== Object.prototype[d] ? c[d] : c[d] = {} : c[d] = b
    }
    

    This code acts like a polyfill, it mutates the global gapi object and assigns ShareClient to gapi.drive.share, everything else is kept hidden from the outside world, inside the anonymous callback, preventing you from modifying it.

    However, nothing prevents you from using a Service Worker as a proxy for this request. A Service Worker will work whatever initiates the request and whatever the target domain is. The only case where you couldn't use a Service Worker is to intercept iframe requests, because an iframe is considered as a separate page, but there is none in your case (the request is initiated by a script which is loaded on the current page).