Security Mechanisms in the Add-on SDK

4

Firefox add-ons have powerful capabilities, including the ability to read and write the user’s file system, access the network, and access stored passwords. At the same time they interact directly with untrusted web content (that is, arbitrary web pages, and the scripts they load). If add-ons aren’t carefully designed, malicious web sites can exploit their vulnerabilities to access these capabilities.

In this post I’ll introduce some of the design elements in the SDK that make it harder for malicious web content to compromise add-ons, and limit the damage done if a site succeeds in compromising all or just part of an add-on.

security layers in the SDK

Layered Defence

The goal of a malicious web page is to reach through the add-on to access the capabilities provided by SDK’s APIs.

Given that the add-on is itself divided between content scripts and the main add-on code, there are four domains involved, with increasingly powerful capabilities:

  1. untrusted web content
  2. content scripts
  3. add-on scripts
  4. SDK modules

At the boundary between each domain, the SDK provides a protection mechanism, as illustrated in the diagram:

  1. content proxies: content scripts access web content via a “content proxy” that protects the add-on from certain attacks
  2. postMessage(): content scripts and add-on scripts can’t access each other’s objects directly but can only send each other messages, using the postMessage() or port.emit() APIs
  3. require(): add-on scripts do not have direct access to all the SDK APIs, but must declare in advance which APIs they intend to use via the require() function

So there are three layers to insulate the powerful capabilities of the SDK from malicious web pages. If one mechanism fails it does not result in a complete security failure, as it only gives access to the next most privileged domain.

Content Proxies

Content scripts in SDK add-ons need to be able to access DOM objects in arbitrary web pages. But malicious pages could redefine standard functions and properties of DOM objects so they don’t do what the add-on expects.

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

1
2
3
4
5
6
7
8
9
10
11
12
<!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>
 
</head>
</html>

content proxy

To deal with this, content scripts access DOM objects via a proxy 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.

So even if a page redefines window.confirm(), a content script which calls that function will get the native implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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. But because it’s 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.

If you really need 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() and see the difference.

Avoid using unsafeWindow if possible: it is the same concept as Greasemonkey’s unsafeWindow, and the scary 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 – although it’s a common enough use case to be able to access the underlying window somehow, so I expect something similar will be provided.

postMessage()

The distinction the SDK makes between “content scripts” and the rest of an add-on’s code is a fundamental part of its design and one that almost all add-on developers need to deal with.

  • the add-on’s main code can use the SDK’s APIs, but can’t access the DOM content of web pages
  • content scripts can access the DOM content of web pages, but can’t use the SDK’s APIs

Thus even the simplest add-on must usually be designed and developed in two parts, with content scripts used to retrieve, inspect, and modify web pages, and the main add-on code used to build user interfaces, make network requests, retrieve stored passwords, and so on.

Content scripts and the main add-on code can only communicate with each other by sending each other messages, and the messages must be JSON-serializable (meaning that, for example, they can’t send each other functions or DOM nodes).

For example, this complete add-on uses a content script to retrieve the HTML of web page elements when the user clicks them:

1
2
3
4
5
6
7
8
9
10
11
12
13
var pageModScript = "window.addEventListener('click', function(event) {" +
                    "  self.postMessage(event.target.innerHTML);" +
                    "}, false);"
 
var pageMod = require('page-mod').PageMod({
  include: ['*'],
  contentScript: pageModScript,
  onAttach: function(worker) {
    worker.on('message', function(HTML) {
      console.log(HTML);
    });
  }
});

This design certainly makes add-on development more complex than it would otherwise be, but it’s not senseless, and one of its main functions is security.

Content scripts execute in a hostile environment, and despite the degree of protection provided by content proxies, there’s a good chance that content scripts will sometimes be successfully compromised by malicious web pages. The distinction the SDK makes between content scripts and the rest of an add-on greatly limits the damage that would result.

Since a content script doesn’t have direct access to any of the SDK APIs, then nor does an attacker who successfully compromises it. The ability to exchange messages with the main add-on is the only extra privilege a malicious page gets as its reward for compromising a content script.

Of course, it’s still possible for an add-on to be vulnerable to maliciously crafted messages, as we’ll see in the next example.

require()

Finally, the add-on code itself doesn’t automatically get access to all the SDK APIs. So even if the add-on code is compromised, the damage it can do is limited to the APIs the add-on explicitly imported using require(). If the add-on didn’t import a module, then the attacker isn’t able to use it.

Consider an add-on like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var sneaky = 'self.postMessage("' +
    'require(\x27notifications\x27).notify({text: \x27I can use this API!\x27});' +
    'require(\x27passwords\x27).search({' +
      'onComplete: function onComplete(credentials) {' +
        'credentials.forEach(function(credential) {' +
          'console.log(credential.username);' +
          'console.log(credential.password);' +
          '});' +
        '}' +
      '});' +
    '");'
 
notifications = require("notifications");
 
var pagemod = require("page-mod").PageMod({
  include: "*.co.uk",
  contentScript: sneaky,
  contentScriptWhen: "start",
  onAttach: function(worker) {
    worker.on("message", function(message) {
      eval(message);
    });
  }
});

This add-on simply calls eval() on any messages it is sent from its content scripts. That clearly causes a complete failure of the security boundary between the content script and the add-on script: any web page able to compromise the content script is trivially able to compromise the add-on code. While this particular message handler has all the hammy villainy of a moustachioed man in a silent film tying a damsel to the train tracks, it’s possible to imagine more subtle security problems which still enable the attacker to run code in the add-on’s context.

The attacker can then access any SDK APIs that the add-on imported. However, the attacker is not able to import any additional SDK APIs. In this example the message attempts to:

  • import the notifications module and use it to print a message
  • import the passwords module and use it to access the user’s stored passwords

If you run the add-on and examine the console output you’ll see that the first succeeds, because the add-on already imported the notifications module:

info: I can use this API!

However, the attempt to use the passwords module fails, with a message like this:

Error: Module at resource://jid1-epalum0vrdz7yq-at-jetpack-sec-lib/main.js
not allowed to require(passwords)

This also means we can set an upper bound for the potential threat an add-on poses, based on the set of modules it imports, and could use that, for example, to determine how rigorous the review process for a particular add-on needs to be.

Categories: general, jetpack

4 responses

  1. Anonymous

    So, the add-on calls require, then calls require again, and then later calls require again, and the third require fails. At what exact point do calls to require start failing?

    1. wbamberg Author

      When the SDK builds your add-on (that is, inside cfx xpi or cfx run) it scans the code looking for “require” statements, and creates a list of the modules you’ve asked for.

      When the SDK evaluates “require” statements at run time it checks that the module you asked for is in the list, and if it’s not, then an exception is thrown (the relevant function is here: https://github.com/mozilla/addon-sdk/blob/master/packages/api-utils/lib/securable-module.js#L243).

      In this example, the scanner finds two “require” statements, at lines 13 and 15, so those two modules get added to the list. This means that the add-on can import and use objects from the notifications and page-mod modules. The scanner doesn’t see the calls at lines 2 and 3, because they’re data, not code (the whole of lines 2-10 could be being read from an outside source such as a web page, and not present in the add-on’s code listing at all – and in a real attack, of course, it would).

      At run time the add-on:
      1- imports notifications at line 13 – that succeeds, because notifications is in the list
      2- imports and uses page-mod at line 15 – that succeeds, because page-mod is in the list
      3- evaluates lines 2-10, turning them from data into code.
      3a – line 2 tries to import notifications, and this succeeds, because notifications is in the list.
      3b – line 3 tries to import passwords, and this fails, because passwords is not in the list

      The point of all this is that if an attacker succeeds in executing their own code inside the add-on, then although they can use any API which the add-on has imported already, they can’t just import additional APIs using “require”.

  2. quamis

    so, when a script is running client-side(through XRayWrapper), it will always see a clean window, instead of the one polluted by libraries loaded by the current page, right?

    Will this proxy always be there? Are there any plans/reasons to ever remove it? Its a nice feature to have..
    I was wondering why i can(and have to) load jquery on the client-side, even if the page already has jquery loaded, this seems to be the reason

    note: your form doen’t accept email like test+t@example.com (it doesn’t handle the ‘+’ part)

    1. wbamberg Author

      > so, when a script is running client-side(through XRayWrapper),
      > it will always see a clean window, instead of the one polluted
      > by libraries loaded by the current page, right?

      Right, which is why, in http://blog.mozilla.org/addons/2011/07/06/porting-the-library-detector-to-the-add-on-sdk/, I needed to use unsafeWindow.

      > Will this proxy always be there?

      I think there will always be some sort of proxy between the content script and the page. The specifics of what is available in it and how you access it might change, although it’s unlikely that things will be removed unless, like unsafeWindow, they’ve been explicitly documented as experimental.

      You can communicate between the content scripts and page scripts using postMessage(), see https://developer.mozilla.org/en/DOM/window.postMessage and https://addons.mozilla.org/en-US/developers/docs/sdk/1.2/dev-guide/addon-development/content-scripts/access.html.

      (But note that you can’t currently use window.postMessage(), you have to use document.defaultView.postMessage() instead. This is the sort of thing I expect will be improved in the future.)