Displaying web content in an extension without security issues

One of the most common security issues with extensions is execution of remote code in privileged context. A typical example is an RSS reader extension that would take the content of the RSS feed (HTML code), format it nicely and insert into the extension window. The issue that is commonly overlooked here is that the RSS feed could contain some malicious JavaScript code and it would then execute with the privileges of the extension — meaning that it would get full access to the browser (cookies, history etc) and to user’s files. These issues can easily be avoided by making use of the existing security mechanisms in the Mozilla codebase. The following article explains these security mechanisms, ideally an extension that needs to display web content (which is always potentially dangerous) will use all of them.

Displaying untrusted data as content

In Firefox, there is a distinction between chrome and content documents. The top document in a window is always a chrome document. If you look at the frames it loads, those will usually be chrome documents as well. However, if the document is loaded into <iframe type="content"> or <browser type="content">, it will be considered a content document, and so will be all the frames it loads (the "type" attribute is ignored at that point). So if we look at the frame hierarchy there is a boundary between chrome and content, and at that boundary a number of security mechanisms apply. In particular, a content document can only go up in the frame hierarchy until the topmost content document, it cannot access the chrome documents above it. This means for example that JavaScript code top.location.href = "about:blank" will only unload the content document but won’t have any effect on the chrome.

Note: This has really nothing to do with the source of the document. If you open "chrome://foo/content/foo.xul" in the browser, it will open as a content document despite having extended privileges. This also means that you won’t be able to establish a security boundary between your extension and untrusted data if your extension opens as a tab in the browser — so displaying your extension in a browser tab is a bad choice.
Note: Dynamic changes of the "type" attribute have no effect, the frame type is read out when the frame element is inserted into the document and never again. So the usual rule is: don’t change the value of the "type" attribute. But if you really have to do this, you will also have to remove the frame element from the document and insert it back.

Not giving privileges to documents that contain untrusted data

The privileges that a document gets depend on where it comes from. For example, "chrome://foo/content/foo.xhtml" will have full privileges, "http://example.com/foo.xhtml" will be allowed to access example.com, "file:///c:/foo.xhtml" will be allowed to read files from disk (with some restrictions). As for the document that displays untrusted data, you don’t want it to have any privileges at all. Here the "data:" protocol is useful. This protocol is special because it inherits the privileges from its parent document. However, if a "data:" document is the topmost content document, there is no parent document (remember, content documents have no access to the chrome documents above them) and consequently no privileges. So in the simplest case you would have:

<iframe type="content" src="data:text/html,%3Chtml%3E%3Cbody%3E%3C/body%3E%3C/html%3E"/>

But usually you don’t want to start with an empty document, you would rather want to load some template into the frame:

var request = new XMLHttpRequest();
request.open("GET", "chrome://foo/content/template.html", false);
request.send(null);
frame.setAttribute("src", "data:text/html," + encodeURIComponent(request.responseText));

That way you can have the template in your extension but still strip it off all privileges when it is loaded in a frame.

Restricting what documents that contain untrusted data can do

There are several restrictions that can be applied per frame. Here it is most important to disable JavaScript and plugins. It won’t harm disabling everything else as well unless it is really required:

frame.docShell.allowAuth = false;
frame.docShell.allowImages = false;
frame.docShell.allowJavascript = false;
frame.docShell.allowMetaRedirects = false;
frame.docShell.allowPlugins = false;
frame.docShell.allowSubframes = false;

But what about interactivity, for example if you want a certain reaction to mouse clicks? This can be done as well, by placing the event handler on the frame tag (meaning that it is outside the restricted document and can execute without restrictions):

<iframe type="content" onclick="handleClick(event);"/>

And the event handler would look like that:

function handleBrowserClick(event)
{
  // Only react to left mouse clicks
  if (event.button != 0)
    return;

  // Default action on link clicks is to go to this link, cancel it
  event.preventDefault();

  if (event.target instanceof HTMLAnchorElement && event.target.href)
    openLinkInBrowser(event.target.href);
}

Safe HTML manipulation functions

When it comes to displaying the data, it is tempting to generate some HTML code and to insert it into the document via innerHTML. And scripts won’t run anyway when inserted via innerHTML, right? Well, not quite. It is right that <script>alert('xss')</script> won’t run if inserted via innerHTML. But <img src="does_not_exist" onerror="alert('xss')"> for example will still run JavaScript code, and there are many more possibilities. So properly sanitizing input is still required when using innerHTML and it is far from trivial.

It is much easier to use DOM manipulation methods that won’t have unexpected side-effects. For example, your template document might have this code:

<style type="text/css">
  #entryTemplate { display: none; }
</style>

<div id="entryTemplate">
  <div class="title"></div>
  <div class="description"></div>
</div>

Now to insert a new entry in the document you would do the following:

var template = doc.getElementById("entryTemplate");
var entry = template.cloneNode(true);
entry.removeAttribute("id");
entry.getElementsByClassName("title")[0].textContent = title;
entry.getElementsByClassName("description")[0].textContent = description;
template.parentNode.appendChild(entry);

The important difference here is that the result will always have the same structure as the template tag. cloneNode() always creates a copy and textContent only manipulates text. So there is no chance of accidentally adding new elements or attributes.

But what if you have to display HTML rather than only text, e.g. an RSS feed entry? Extension authors will often come up with flawed attempts to sanitize HTML code. Instead, nsIScriptableUnescapeHTML.parseFragment() method should be used that is meant for just that scenario:

var target = entry.getElementsByClassName("description")[0];
var fragment = Components.classes["@mozilla.org/feed-unescapehtml;1"]
                         .getService(Components.interfaces.nsIScriptableUnescapeHTML)
                         .parseFragment(description, false, null, target);
target.appendChild(fragment);

This will add the HTML code to the specified node — minus all the potentially dangerous content.

See also

Original Document Information

  • Author(s): Wladimir Palant
  • Last Updated Date: 2009-01-28