proxy

Content scripts need access to the DOM of the pages they are attached to. However, those pages should be considered to be hostile environments: we have no control over any other scripts loaded by the web page that may be executing in the same context. If the content scripts and scripts loaded by the web page were to access the same DOM objects, there are two possible security problems:

First, a malicious page might redefine functions and properties of DOM objects so they don't do what the add-on expects. For example, if a content script calls document.getElementById() to retrieve a DOM element, then a malicious page could redefine its behavior to return something unexpected:


// If the web document contains the following script:
document.getElementById = function (str) {
  // Overload indexOf method of all string instances
  str.__proto__.indexOf = function () {return -1;};
  // Overload toString method of all object instances
  str.__proto__.__proto__.toString = function () {return "evil";};
};
// After the following line, the content script will be compromised:
var node = document.getElementById("element");
// Then your content script is totally out of control.

Second, changes the content script made to the DOM objects would be visible to the page, leaking information to it.

The general approach to fixing these problems is to wrap DOM objects in XrayWrappers (also know as XPCNativeWrapper). This guarantees that:

  • when the content script accesses DOM properties and functions it gets the original native version of them, ignoring any modifications made by the web page
  • changes to the DOM made by the content script are not visible to scripts running in the page.

However, XrayWrapper has some limitations and bugs, which break many popular web frameworks. In particular, you can't:

  • define attributes like onclick: you have to use addEventListener syntax
  • overload native methods on DOM objects, like this:
    
    proxy.addEventListener = function () {};
    
  • access named elements using properties like window[framename] or document[formname]
  • use some other features that have bugs in the XrayWrapper implementation, like mozMatchesSelector

The proxy module uses XrayWrapper in combination with the experimental Proxy API to address both the security vulnerabilities of content scripts and the limitations of XrayWrapper.

  /--------------------\                           /------------------------\
  |    Web document    |                           | Content script sandbox |
  | http://mozilla.org |                           |     data/worker.js     |
  |                    | require('content-proxy'). |                        |
  | window >-----------|-     create(window)      -|-> window               |
  \--------------------/                           \------------------------/

The Big Picture

The implementation defines two different kinds of proxy:

  1. Content script proxies that wrap DOM objects that are exposed to content scripts as described above.
  2. XrayWrapper proxies that wrap objects from content scripts before handing them over to XrayWrapper functions. These proxies are internal and are not exposed to content scripts or document content.
  /--------------------\                           /------------------------\
  |    Web document    |                           | Content script sandbox |
  | http://mozilla.org |                           |     data/worker.js     |
  |                    |                   /-------|-> myObject = {}        |
  |                    |  /----------------v--\    |                        |
  |                    |  | XrayWrapper Proxy |    | - document             |
  |                    |  \---------v---------/    \----^-------------------/
  |                    |            v                   |
  |                    |  /-------------\  /----------\ |
  | - document >-------|->| XrayWrapper |<-| CS proxy |-/
  \--------------------/  \-------------/  \----------/

Everything begins with a single call to the create function exported by the content-proxy module:

// Retrieve the unwrapped reference to the current web page window object
var win = gBrowser.contentDocument.defaultView.wrappedJSObject;
// Or in addon sdk style
var win = require("tab-browser").activeTab.linkedBrowser.contentWindow.wrappedJSObject;
// Now create a content script proxy for the window object
var windowProxy = require("api-utils/content/content-proxy").create(win);

// We finally use this window object as sandbox prototype,
// so that all web page globals are accessible in CS too:
var contentScriptSandbox = new Cu.Sandbox(win, {
  sandboxPrototype: windowProxy
});

Then all other proxies are created from this one. Attempts to access DOM attributes of this proxy are trapped, and the proxy constructs and returns content script proxies for those attributes:

// For example, if you simply do this:
var document = window.document;
// accessing the `document` attribute will be trapped by the `window` content script
// proxy, and that proxy will that create another content script proxy for `document`

So the main responsibility of the content script proxy implementation is to ensure that we always return content script proxies to the content script.

Internal Implementation

Each content script proxy keeps a reference to the XrayWrapper that enables it to be sure of calling native DOM methods.

There are two internal functions to convert between content script proxy values and XrayWrapper values.

  1. wrap takes an XrayWrapper value and wraps it in a content script proxy if needed. This method is called when:
    • a content script accesses an attribute of a content script proxy.
    • XrayWrapper code calls a callback function defined in the content script, so that arguments passed into the function by the XrayWrapper are converted into content script proxies. For example, if a content script calls addEventListener, then the listener function will expect any arguments to be content script proxies.

  2. unwrap takes an object coming from the content script context and:
    • if the object is a content script proxy, unwraps it back to an XrayWrapper reference
    • if the object is not a content script proxy, wraps it in an XrayWrapper proxy.

      This means we can call a XrayWrapper method either with:

      • a raw XrayWrapper object.

        // The following line doesn't work if child is a content script proxy,
        // it has to be a raw XrayWrapper reference
        xrayWrapper.appendChild(child)
        
      • an XrayWrapper proxy when you pass a custom object from the content script context.

        var myListener = {
          handleEvent: function(event) {
            // `event` should be a content script proxy
          }
        };
        // `myListener` has to be another kind of Proxy: XrayWrapper proxy,
        // that aims to catch the call to `handleEvent` in order to wrap its
        // arguments in a content script proxy.
        xrayWrapper.addEventListener("click", myListener, false);
        

Stack Traces

The following code:

function listener(event) {

}
csProxy.addEventListener("message", listener, false);

generates the following internal calls:

-> CS Proxy:: get("addEventListener")
  -> wrap(xrayWrapper.addEventListener)
    -> NativeFunctionWrapper(xrayWrapper.addEventListener)
      // NativeFunctionWrapper generates:
      function ("message", listener, false) {
        return xraywrapper.addEventListener("message", unwrap(listener), false);
      }
      -> unwrap(listener)
        -> ContentScriptFunctionWrapper(listener)
        // ContentScriptFunctionWrapper generates:
        function (event) {
          return listener(wrap(event));
        }

// First, create an object from content script context
var myListener = {
  handleEvent: function (event) {

  }
};
// Then, pass this object as an argument to a CS proxy method
window.addEventListener("message", myListener, false);

// Generates the following internal calls:
-> CS Proxy:: get("addEventListener")
  -> wrap(xrayWrapper.addEventListener)
    -> NativeFunctionWrapper(xrayWrapper.addEventListener)
       // Generate the following function:
       function ("message", myListener, false) {
          return xraywrapper.addEventListener("message", unwrap(myListener), false);
       }
       -> unwrap(myListener)
         -> ContentScriptObjectWrapper(myListener)
            // Generate an XrayWrapper proxy and give it to xrayWrapper method.
            // Then when native code fires an event, the proxy will catch it:
            -> XrayWrapper Proxy:: get("handleEvent")
              -> unwrap(myListener.handleEvent)
                -> ContentScriptFunctionWrapper(myListener.handleEvent)
                   // Generate following function:
                   function (event) {
                     return myListener.handleEvent(wrap(event));
                   }

API Reference

Functions

create(object)

Create a content script proxy.
Doesn't create a proxy if we are not able to create a XrayWrapper for this object: for example, if the object comes from system principal.

object : Object

The object to proxify.

Returns: Object

A content script proxy that wraps object.