Skip to content
This repository has been archived by the owner on Feb 26, 2022. It is now read-only.

Privileged object export

ochameau edited this page Mar 12, 2013 · 11 revisions

COW and createObjectIn

When a pure JavaScript object needed to be exposed from privileged scope to content, because of the flexibility of the language it is really difficult to do it in a safe way. There were two API so far for this, one is COW, which is a filtering wrapper, with the clumsy __exposedProps__ property and with various hacks in it's implementation around it's prototype chain.

And the createObjectIn / makeObjectPropsNormal function pair. This later API enabled one to create an object in the content compartment and then add safe privileged methods bound to the (privileged) object to be exposed.

COWs turned out to have a lot of problems, the old version with security the newer one with the prototype chain trick fixed that issue but had some other problems with the js engine in some edge cases. So it will be deprecated. The other API is quite difficult to use really, and has it's own issues. Since for jetpack a well designed API that is reliable is needed we came up with a 3rd version that is somewhat closer to the createObjectIn / makeObjectPropsNormal version.

Ongoing issues with COW

COW for jetpack are currently unsafe. Exposed object without __exposedProps__ set are fully exposed.

Otherwise everywhere in Firefox codebase: JSM, XPCOM, chrome documents, the default behavior is to reject any chrome object property access if __exposedProps__ isn't explicitely defined... except in sandboxes. That's just to avoid breaking old addons. (See bug 553102)

Having said that, we are close to make them safe as these old addons start to have been repacked or with very few users.

nsExpandedPrincipals

nsEp is an array of principals. It gives access to all those origins, in this way it has more privileges than a regular principal. Because of that it will result an async security relationship between content and content-script. Content-script will have full access to content but not the other way around. So in case we want to expose an object from content-script to content we will have to use a similar API as for the system-content case. As it turns out this security restriction can help us avoid some security leaks, so we might want to use nsEp always. In the regular case that would be an array with a single element, the content principal. That nsEp would not have access to any other domain, but would preserve this async security relationship between content-script and content.

Where do we need this?

This API is going to be needed for:

  • every content-script that wants to expose JS Object to content
  • modules want to expose JS object to content or content-script

The new and shiny API

Before I define the API I try to explain the concept in a nutshell. The API creates two constructor. One is accessible from privileged code (host), and one optional that is accessible from content side (client). Calling each will execute both, and will create two objects that are linked together under the hood. One on host side and one on client side. I will call these objects host view and client view for now. By linked I mean if we access it from the host scope we will get the host view of the object if we access it from client scope we will get the client view. There will be an array of privileged methods which will be installed on the client view, but will be executed in host scope (which means the |this| object will be the host view).

The API could use some name changes...

Cu.defineClientAPI(options);

The options object:

  • clientGlobal: the global object of the target compartment (content window usually) required
  • hostConstructor: constructor code for host-side objects
  • clientConstructor: toSourced()/eval-ed constructor code for client-side objects
  • clientConstructorName: optional name. If given, client-side constructor will be defined on client global.
  • hostPrototype: prototype for host-side objects
  • clientPrototype: structured cloned into content, used as prototype for client-side objects
  • clientAPI: {prop: { getter: foo, setter: bar}, prop: {value: func; }}
    • Privileged functions that get defined on client-side prototype.
    • When invoked, run in host, with host-side object as |this|
    • These functions in their client-side form require a bonafide client object to operate.

Finally there is a simplified version for cloning object into the other compartment: Cu.cloneInto(global, object); This version only does a structured cloning into the other compartment, without any privileged methods, or tricks. It's almost like passing by JSON. Personally I don't find this one very interesting.

Examples

Example 1 - simple use case

privileged scope:

var Cu = ...

var exportedObjectCtor = Cu.defineClientAPI({
  clientGlobal: contentWindow,
  clientConstructorName: PrivObject,
  clientAPI: {privMethod: {value: function(){
                                    Cu.somePrivilegedFunction();
                                    if (this.contentProp)
                                      throw "this should not be defined on the chrome view of the object";
                                  }
                           }
              }
});

var exportedObject = new exportedObjectCtor();
exportedObject.notExportedProp = 5;

contentWindow.someContentFunction(exportedObject);

content scope:

function someContentFunction(privObject) {
  privObject.privMethod();
  if (privObject.notExportedProp)
    throw "this should not be defined on the content view of the exported object"
}

var privObject = new PrivObject();
privObject.contentProp = 42;
privObject.privMethod();

Eventual content script API

Just shooting draft examples, voluntary abstracted from the current platform proposal in order to adapt the api to concrete usecases.

Webapi on navigator that returns a promise-like object

// The advantage of this function is that we pass `navigator`. It allows to know for which domain
// this content object should be created (in case of multiple domain)
// (And this pattern is almost the same than nsIDOMGlobalPropertyInitializer)
self.expose(navigator, "myapi", {
  doSomething: function () {
    let resolved;
    let _onsuccess;
    ...
    setTimeout(function () {
      // Just allow exposing simple JS object but that can also contain xrays.
      // They should be pass along without any issue/more work
      resolved = self.expose2({
        value: "ok",
        target: domNode
      });
      if (_onsuccess)
        _onsuccess(resolved);
    }, 1000);
    ...
    // This one could gain from the constructor idiom
    return self.createContentObject({
      get onsuccess(v) {return _onsuccess;}
      set onsuccess(v) {
        _onsuccess=v;
        if (resolved)
          v(resolved);
      }
    });
  }
});
/// --- usage in web page
let request = navigator.myapi.doSomething();
request.onsuccess = function (result) {
  node.textContent = result.value + "/" + result.target.dataset["attr"];
}

Hook website existing function

That example is the most typical usage of unsafeWindow in jetpack. It is done to either modify the behavior of an existing website method, or be able to know when a particular event happens.

let original = window.songProgress;
self.overload(window, "songProgress", function (p) {
  self.emit("song-progress", p);
  original(p);
});

Webpage code:

function songProgress(percent) {
  ctx.drawRect(0, 0, percent*canvas.width, canvas.height);
}

Do ask questions and tell me about your ideas!

https://etherpad.mozilla.org/cvbBmdFNXX

Clone this wiki locally