Displaying web content in an extension – without security issues

Over the last few years I reported a bunch of security vulnerabilities in various extensions and by far the most common issue was: “Extension Foo allows execution of remote code in privileged context”. Typically, an RSS reader extension would take the content of the RSS feed (HTML code), format it nicely and insert into the extension window. The issue that is overlooked here is that the RSS feed could contain some JavaScript code and it would then execute with the privileges of the extension — meaning for example that it would get full access to the browser (cookies, history etc) and to user’s files. pdp discovered a similar issue in the Firebug extension that uses an HTML-based templating system and forgot to sanitize some input received from the webpage. (Clarification: The Firebug vulnerability is a very old one, this wasn’t meant as an example of an open vulnerability)

In my opinion, all these problems could have easily been avoided by choosing the right approach that makes use of the existing security mechanisms in the Mozilla codebase. For JavaScript Deobfuscator 1.5 I had to display JavaScript code from potentially malicious websites in my own window so I decided to take as many precautions as possible. And it is really not that hard.

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 the document containing untrusted data privileges

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);
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 the document containing 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)

  // Default action on link clicks is to go to this link, cancel it

  if (event.target instanceof HTMLAnchorElement && 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; }

<div id="entryTemplate">
  <div class="title"></div>
  <div class="description"></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.getElementsByClassName("title")[0].textContent = title;
entry.getElementsByClassName("description")[0].textContent = description;

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"]
                         .parseFragment(description, false, null, target);

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


  • Cesar

    I’ve had a problem before where the MDC XUL documentation for iframe doesn’t seem to mention the type attribute, but for browser it does. And not that many people know whether it does follow the type attribute. Thanks for bringing this up.

  • Mook

    Note that things like the docshells are not frozen, which means it’s free to change between security releases (3.0.x -> 3.0.x+1). Just like everything else useful in the Mozilla world…

    Wladimir Palant

    I think you are misunderstanding something, minor releases aren’t supposed to change XPCOM interfaces – as it is now, the Mozilla project tends to consider all the XPCOM interfaces, frozen or not, as “the API”. Of course the interface could change between major releases, but then again – everything can change between major releases, and extension developers usually have to worry about browser UI changes far more than about incompatible XPCOM interface changes.

  • Arne

    I assume a test for this could be programmed. Shouldn’t such a test be mandatory for publication of extensions on addons.mozilla.org ?

  • Eric Shepherd

    This should be on MDC… any interest in migrating it? I can do it but if it goes on my queue of stuff, it will be a long time before it happens. :)

    Wladimir Palant

    Unfortunately, my queue is also pretty long :)

  • Eric Shepherd

    Well, I can do a quick migrate of this article into MDC if that’s okay with you, and we can let it get twiddled if necessary as time allows. Would that be all right?

    Wladimir Palant

    Sure, that’s fine with me.

  • Eric Jung

    On a related topic, if you need to execute remote javascript from within an extension, but don’t want to grant that javascript elevated privileges, use Components.utils.evalInSandbox():


  • Eric Shepherd

    This entry has been duplicated into MDC:


    Wladimir Palant

    Thanks. I rewrote the introduction so that it makes sense in an article.

  • Mook

    Ooops, sorry for forgetting about this.

    A minor release did change an interface in 3.0.2. And caused people with that extension to crash. And people were all blaming the extension for using an internal interface. See bug 455283 (RoboForm, nsIFrame). Granted, that’s more likely to change than docshell; but I was told docshell will never be frozen anyway, too, so it’s also free to blow up.

    Unless an interface is explcitly marked frozen, it probably will blow up.

    Talking about things from (somebody else’s) experience sucks.

    Wladimir Palant

    You are talking about a binary extension that accessed an internal interface that isn’t even exposed through XPCOM (yes, I fully agree with the first comment in the bug). I hope you see the difference to JavaScript accessing nsIDocShell.

  • Gabe

    Thanks for this useful info. When displaying html from an external source (rss reader) is there is anyway to deal with mismatched tags in the html and prevent them from affecting the rest of the content on the page? Right now I insert every html block into its own xul:browser element, but this creates performance issues.