Add-on SDK FAQ: Content Script Access

Jeff Griffiths

13

Update: the SDK 1.2 beta builds ( including this one, just released today ) now include a more refined version of this content. If you’re interested in this material I strongly suggest that you download this beta and take a look at the bundled docs.

This post is an attempt to clearly explain how content scripts work. I am posting this particular topic as a blog post in hopes of getting feedback on the wording, but the content below is actually part of a larger patch against the SDK documentation that Will Bamberg created as part of this SDK bug. The specific purpose of this post is to explain the access that content scripts have to:

  • DOM objects in the pages they are attached to
  • other content scripts
  • other scripts loaded by the page they are attached to

Access to the DOM

Content scripts need to be able to access DOM objects in arbitrary web pages, but this gives rise to a potential security problem. A malicious page could redefine standard functions and properties of DOM objects so they don’t do what the add-on expects. To deal with this, content scripts access DOM objects via a proxy. Any changes they make are made to the proxy.

The proxy is based on XRayWrapper, (also known as XPCNativeWrapper). These wrappers give the user access to the native values of DOM functions and properties, even if they have been redefined by a script.

For example: the page below redefines window.confirm() to return true without showing a confirmation dialog:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang='en' xml:lang='en' xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <script>
    window.confirm = function(message) {
      return true;
    }
    </script>
<p></head>
</html>

Thanks to the content proxy, a content script which calls window.confirm() will get the native implementation:

var widgets = require("widget");
var tabs = require("tabs");
var data = require("self").data;
 
var widget = widgets.Widget({
  id: "transfer",
  label: "Transfer",
  content: "Transfer",
  width: 100,
  onClick: function() {
    tabs.activeTab.attach({
      // native implementation of window.confirm will be used
      contentScript: "console.log(window.confirm('Transfer all my money?'));"
    });
  }
});
 
tabs.open(data.url("xray.html"));

You can try this example at: https://builder.addons.mozilla.org/addon/1013777/revision/4/.

The proxy is transparent to content scripts. As far as the content script is concerned, it is accessing the DOM directly, however because it is not, some things that you might expect to work, won’t. For example, if the page includes a library like jQuery, or any other page script adds any other objects to the window, they won’t be visible to the content script. So to use jQuery you’ll typically have to add it as a content script, as in this example.

unsafeWindow

If you really need direct access to the underlying DOM, you can use the global unsafeWindow object. Try editing the example at https://builder.addons.mozilla.org/addon/1013777/revision/4/ so the content script uses unsafeWindow.confirm() instead of window.confirm() to see the difference.

Avoid using unsafeWindow if possible! It is the same concept as Greasemonkey’s unsafeWindow, and the warnings for that apply equally here. Also, unsafeWindow isn’t a supported API, so it could be removed or changed in a future version of the SDK.

Access to Other Content Scripts

If you load several content scripts loaded into the same document can interact with each other directly as well as with the web content itself. This allows you to do things like load jQuery and then load your own scripts that use jQuery. Content scripts which have been loaded into different document cannot interact with each other.

For example:

  • if an add-on creates a single panel object and loads several content scripts into the panel, then they can interact with each other.
  • if an add-on creates two panel objects and loads a script into each one, they cannot interact with each other.
  • if an add-on creates a single page-mod object and loads several content scripts into the page mod, then only content scripts associated with the same page can interact with each other: if two different matching pages are loaded, content scripts attached to page A cannot interact with those attached to page B.

The web content has no access to objects created by the content script, unless the content script explicitly makes them available. For example, you could expose a variable in your content script directly to web content by doing something like this:

Access to Page Scripts

You can communicate between the content script and page scripts using postMessage(), but there’s a twist: in early versions of the SDK, the global postMessage() function in content scripts was used for communicating between the content script and the main add-on code. Although this has been deprecated in favor of self.postMessage, the old globals are still supported, so you can’t currently usewindow.postMessage(). You must use document.defaultView.postMessage() instead.

The following page script uses window.addEventListener to listen for messages:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang='en' xml:lang='en' xmlns="http://www.w3.org/1999/xhtml">
 
<head>
    <script>
      window.addEventListener("message", function(event) {
        window.alert(event.data);
      }, false);
    </script>
</head>
</html>

Content scripts can send it messages using document.defaultView.postMessage():

var widgets = require("widget");
var tabs = require("tabs");
var data = require("self").data;
 
var widget = widgets.Widget({
  id: "postMessage",
  label: "demonstrate document.defaultView.postMessage",
  contentURL: "http://www.mozilla.org/favicon.ico",
  onClick: function() {
    tabs.activeTab.attach({
      contentScript: "document.defaultView.postMessage('hi there!', '*');"
    });
  }
});
 
tabs.open(data.url("listener.html"));

You can see this add-on at https://builder.addons.mozilla.org/addon/1013849/revision/8/.

13 responses

  1. Derrick Rice (:drice) wrote on :

    A few basic questions, then some open-ended ones:

    * When you say SDK, do you mean AddOn SDK or Gecko SDK? i.e. is this specific to uses of the AddOn SDK?
    * Can you link to documentation on how to create Content Scripts?
    * It sounds like this will be the only way to access content DOM once the multiprocess changes are made. Is that true?

    * How does this affect or compare to using contentDocument.wrappedJSObject ?
    * How do Content Scripts compare to using JavaScript-global-property ?

    There is no an example of a Page Script communicating *to* a Content Script. As far as I can tell, it shouldn’t be necessary for Content Scripts and Page Scripts to communicate via postMessage when they exist in the same process/thread.

    It’s important to my extension that the Page be able to pass a DOM object to my Component (trusted) code and that the Component code knows it is working with an XRayWrapper object. Right now I do that by implementing a JavaScript-global-property and accessing wrappedJSObject when I am explicitly interested in getting at Page Script defined values. I don’t see how I’d transition to the new system.

    1. Jeff Griffiths wrote on ::

      Hi Derrick,

      Thanks for your response! I’ve attempted to provide some answers to your questions below:

      1. All uses of the word ‘SDK’ refer to the Add-on SDK. This post does not cover any usage of the larger Gecko APIs.

      2. Content scripts are used extensively in the SDK, you can find examples of typical usage in the docs for these two modules:

      tabs: https://addons.mozilla.org/en-US/developers/docs/sdk/1.0/packages/addon-kit/docs/tabs.html

      page-mod: https://addons.mozilla.org/en-US/developers/docs/sdk/1.0/packages/addon-kit/docs/page-mod.html

      Content scripts are the generic method the SDK has for injecting JavaScript into a lower-privilege document from your higher-privilege add-on code. Note that you can supply both a string with some JavaScript to be evaluated as well as paths to entire files and libraries to include.

      3. The intention is that the existing message-passing pattern will continue to work once Firefox becomes multi-process. We feel strongly that developers using the SDK should not have to significantly alter their code to work once these changes land. Traditional extensions may have a harder time adjusting, but I understand there is work being done within the platform group and by the AMO team to mitigate problems from this transition.

      4. Your content scripts currently have access to unsafeWindow, which is direct access to the content’s DOM. I believe this is a similar level of access as wrappedJSObject, however unsafeWindow exists only in the content script, not in the main add-on code. It is a security feature of the SDK that we only allow postMessage-style communications between add-on code and content script code.

      5. I’m not entirely clear what technique you’re referring to when you say ‘JavaScript-gobal-property’ – do you mean a global at the chrome level? If it helps, the SDK’s design is heavily bent towards compartmentalization, in part to bake in a security model that does not exist with traditional add-ons, and in part to prepare for multi-process.

      Curious, can you link me to your add-on? I’m keenly interested in identifying patterns in existing add-ons that the SDK fails to provide. I am betting that a lot of the steps the SDK takes to ensure compartmentalization will get in your way; I still think it is worth-while considering the trade-offs and seeing if we can’t work around them.

      My main curiosity is: will the SDK’s security model mean that your add-on is impossible to implement, or instead will it necessitate a major re-factoring.

      cheers, Jeff

      1. Derrick Rice (:drice) wrote on :

        ‘JavaScript-gobal-property’ is an identifier for the category manager. A component in that category has an instance created for each content page (page scripts) and is installed as window.$IDENTIFIER.

        This allows me to expose features implemented in a privileged (trusted) component to page scripts via direct JavaScript interaction. Any arguments to privileged functions are automatically wrapped in XRayWrappers / XPCNativeWrappers, protecting the privileged component from making decisions based on untrusted data.

        This isn’t widely documented, by any measure. It took some trial and error to determine the exact security features I was interacting with, and how to get “around” them to allow the page to interact with components in a controlled manner. I believe nsScriptNameSpaceManager is responsible for this magic.

        http://weblogs.mozillazine.org/weirdal/archives/017188.html
        https://developer.mozilla.org/en/nsICategoryManager

        window.console and window.InstallTrigger work this way, too:

        https://developer.mozilla.org/en/XPInstall_API_Reference/InstallTrigger_Object
        http://mxr.mozilla.org/mozilla-central/ident?i=JAVASCRIPT_GLOBAL_PROPERTY_CATEGORY

  2. John Nagle wrote on ::

    This makes sense, but it’s still not clear how all the pieces fit together. I’m trying to convert some reasonably complex Greasemonkey add-ons to the new Mozilla add-on interface.

    1. Does the “contentScript” of a PageMod object of the “page-mod” API see the wrapped document object, as in Greasemonkey?

    2. Is there anything corresponding to GM_xmlhttpRequest in this environment? Or do I have to explicitly do inter-process communication with the page script?

    3. I want to maintain a persistent local cache in my add-on. Currently I do that with GM_getvalue/GM_setvalue. Should I use DOM storage? MozStorage?

    4. Clearly this and the “page-mod” API are intended to provide the features Greasemonkey provides. Is there a Greasemonkey compatibility package somewhere?

    1. Jeff Griffiths wrote on ::

      Hi John, thanks for your comments! In answer:

      1. yes, in the content script, the wrapped document object is ‘window’. If you want an unwrapped version then, lke in Greasemonkey, we have ‘unsafeWindow’ as well, with the obvious related security concerns.

      2. for same-domain access you can make xhr requests as normal in the content script. For example, if you load jQuery in your contentScriptFile list, you can then do $.get(…). If you want cross-domain requests, you should instead use the Request code in main.js.

      3. the SDK provides the simple-storage api: https://addons.mozilla.org/en-US/developers/docs/sdk/1.1/packages/addon-kit/docs/simple-storage.html If you have more complex needs you could create a CommonJS module that provides access to indexdb.

      4. the SDK doesn’t provide a Greasemonkey compatibility bridge, and we don’t currently have plans to. One of the key things about the SDK however is that it is extensible, and we’re thinking about how to promote and support this extensibility. It should be totally possible to implement something using the SDK that runs Greasemonkey scripts, however I suspect this will be a significant task.

  3. John Nagle wrote on ::

    Thanks.

    Now I’m struggling with visibility and namespace issues as I convert Greasemonkey code.

    1. “contentScript” is a string which is executed in the Javascript context of the page. So I can’t access any of the add-on’s code from there. That code executes in a different namespace. So how can I get to the add-on script’s code from code in “contentScript”?

    2. “require” in Greasemonkey imports top-level objects into the global namespace of the add-on. “require” in the new Mozilla add-on API imports the file into a variable. If I import two files with “require”, they can’t see each other’s top-level names. It looks like each file has to “require” everything it needs, and must then reference all imported names relative to the name under which the file was imported. Is that right, or is there an easier way?

  4. John Nagle wrote on ::

    Disregard previous question. I’ve figured out how to load my add-on code into the content side.

  5. John Nagle wrote on ::

    There’s a real show-stopper in converting Greasemonkey plugins, or anything that uses XMLHttpRequest – the add-on API doesn’t support XML. In a quick 30-hour purge last year, some developers pulled XML support. See

    https://bugzilla.mozilla.org/show_bug.cgi?id=611042

    It looks like the add-ons group has a You Will Use JSON Or Else attitude.

    (Yes, I suppose some complex workaround involving sending XML to the content script to be parsed and returned is possible. Possible, but lame.)

    1. Jeff Griffiths wrote on ::

      Hi John,

      You’re absolutely right, we don’t currently support XML particularly well. I should point out for the future that, on our roadmap is a section called ‘hug the web harder’:

      https://wiki.mozilla.org/Features/Jetpack/Make_Add-on_SDK_Hug_the_Web_Harder

      One of the items that falls under this is some work to expose the full XMLHttpRequest object in the addon code scope. There are a few other rough edges similar to this that we are tracking as well.

  6. John Nagle wrote on ::

    I gave up on XML and added JSON capability to the server to which my add-on talks. Sigh.

    Next problem: I have some kind of race condition in DOM updates that doesn’t occur with Greasemonkey. My add-on modifies a page by adding rating icons to links. At page startup, each link gets an “in progress” rating icon, which is an animated GIF image. That’s done by the content script. The domains involved are then sent to the add-on code over a port, where a Request is made to a remote server. When a reply from the server comes back, the callback sends replies over a port back to the content script, where an “on” function then replaces the icons with appropriate rating icons.

    Most of the time, this works. But sometimes, the “in progress” icon never gets visibly replaced. I have lots of logging info running under “cfx run”, and I can see the events that update the DOM with a new icon. The DOM is being updated with the new icon, but it doesn’t get rendered.

    If I disable the code that inserts the “in progress” icon, the rating icons always appear properly, so the icon insertion works. Trouble occurs when two updates to the icon are made in rapid succession before the page gets re-rendered. Are there any known race conditions in that area? I’m running under cfx, and with a lot of logging turned on, so it takes seconds for this to happen. That may affect the timing. (Incidentally, why does running under cfx with logging do disk I/O each time a console log message is printed?)

    The add-on code that updates the DOM is shared with the Greasemonkey plug-in version (they even share most of the smae Javascript files), which works fine. So I don’t think it’s that part of the system.

    I know, this isn’t enough information. I’m trying to narrow this down. My question is whether it’s possible that two updates to the DOM made before the page re-renders could be done out of order.

    1. Jeff Griffiths wrote on ::

      Hi John,

      I think you are right that there is likely a race condition involved, perhaps introduced through an interaction in how the SDK applies content scripts to the page and how your code works. My best suggestion for getting help would be to re-post this to the Jetpack google group:

      http://groups.google.com/group/mozilla-labs-jetpack/

      If possible, it would be great to take a look at your code as well.

      1. John Nagle wrote on ::

        It turns out that there’s some intermittent bug in “element.getElementsByTagName” which appears only in Jetpack add-ons, not in Greasemonkey code. About 10% of the time, “element.getElementsByTagName” returns an empty array. Checking code verifies this. Workaround code has been posted.

        Discussed on Jetpack forum:

        https://forums.mozilla.org/addons/viewtopic.php?f=27&t=4056&p=12475#p12475

        Bug report filed:
        https://bugzilla.mozilla.org/show_bug.cgi?id=693076

        1. Jeff Griffiths wrote on ::

          John: thanks for following up with that groups post & bug report, we really appreciate your time and effort!