Search code examples
javascriptfirefoxxmlhttprequestfirefox-addonfirefox-addon-webextensions

Override functions of the page in a Firefox extension content script


I make a firefox extension that get all the request url's and displays them. But the code only works if I paste it in the console.

when the extension loads it doesn't show any error, it seems like it just won't run

here is the full code

xhrScript.js

(function(){

    const proxiedOpen = XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function ( _, url) {
        this.__URL = url;
        return proxiedOpen.apply(this, arguments);
    };

    const proxiedSend = window.XMLHttpRequest.prototype.send;
    window.XMLHttpRequest.prototype.send = function () {
        const { protocol, host } = window.location;
        // showing only when it paste in console
        console.log("full request url ", `${protocol}//${host}${this.__URL}`);
        return proxiedSend.apply(this, [].slice.call(arguments));
    };

})();

// this works all times
document.body.style.border = "7px solid blue";

manifest.json

{
    "manifest_version": 2,
    "name": "XHR request urls",
    "version": "1.0",
    "description": "get all the request url's",

    "content_scripts": [
      {
        "matches": ["*://*/*"],
        "js": ["xhrScript.js"]
      }
    ]  
}

As you can see, in the last line is document.body.style.border = "7px solid blue";, this works fine every time. But the XMLHttpRequest open and send methods don't work. only works if I paste the code in the console.

if you want see an example, you can try copy and paste the xhrScript.js code in https://reactjs.org (it's a SPA, so it's easy to check what I want) in the devTools console, and see all the request.

I don't know why this code only runs when it is pasted in console


Solution

  • Content scripts run in an isolated JavaScript environment meaning that window and its contents are isolated from the page so when you modify it, you only modify the content script's version.

    There are two solutions:

    1. Firefox-specific.

      Use wrappedJSObject and exportFunction to access the page context (more info):

      const urls = new WeakMap();
      const origXhr = hookPagePrototype('XMLHttpRequest', {
        open(method, url) {
          urls.set(this, url);
          return origXhr.open.apply(this, arguments);
        },
        send() {
          console.log('Sending', new URL(urls.get(this), location).href);
          return origXhr.send.apply(this, arguments);
        },
      });
      
      function hookPagePrototype(protoName, funcs) {
        const proto = wrappedJSObject[protoName].prototype;
        const oldFuncs = {};
        for (const [name, fn] of Object.entries(funcs)) {
          oldFuncs[name] = exportFunction(proto[name], wrappedJSObject);
          proto[name] = exportFunction(fn, wrappedJSObject);
        }
        return oldFuncs;
      }
      
    2. Chrome-compatible.

      Use a DOM script to run the code in page context: instruction.

      It won't work on pages protected by a strict Content-Security-Policy (CSP) that prevents script execution so when writing an extension for Firefox we should use wrappedJSObject method instead.