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.
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:
- untrusted web content
- content scripts
- add-on scripts
- SDK modules
At the boundary between each domain, the SDK provides a protection mechanism, as illustrated in the diagram:
- content proxies: content scripts access web content via a “content proxy” that protects the add-on from certain attacks
- 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()
orport.emit()
APIs - 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:
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:
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:
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:
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.
Anonymous wrote on
wbamberg wrote on
quamis wrote on
wbamberg wrote on