Security Review: HTML sanitizer in Thunderbird

I spent a few days working on a security review for Thunderbird's HTML sanitizer. Thunderbird has three presets for viewing mail: Original HTML, Simple HTML, and Plain Text. No matter which preset the user prefers, emails should not execute JavaScript. And this is where the HTML sanitizer joins our party.

This security review was discussed in one of my first weeks at Mozilla and though being a very interesting topic, it soon occured to me that I might have bitten off more than I could chew. So the security review got stuck in my queue and I finally dared to take a stab at it months later. (Thanks to those fellow Mozillians who helped me getting started!)

The key lesson about HTML sanitizers is: Don't even consider writing your own.

So without further ado, I started collecting bits and pieces together. First I required creating a recent build of Thunderbird. Then I looked into XPCShell tests (unit tests using Mozilla's privileged JavaScript libraries) and the nsIParserUtils interface. My next step was writing a basic sanitizer call, and it turned out comparably easy:

var ParserUtils =  Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
var sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly|ParserUtils.SanitizerDropForms|ParserUtils.SanitizerDropNonCSSPresentation;
var output = ParserUtils.sanitize("XXX HTML here", sanitizeFlags);

With this prototype, I could easily loop around a dataset of HTML vectors. For this I chose the vectors from the html5 security cheat sheet and RSnake's old XSS cheat sheet (thank you guys!)

Thankfully the html5 security cheat sheet has its attacks in a JSON file. Extracting them was as easy as taking this dataset and joining the vectors with the file that contains the actual attack payload, (i.e., JavaScript alerts and other triggers in various encodings). The XPCShell comes with a load() function which makes it very easy to include these JSON files.

The full test then looks a bit like this:

var Ci = Components.interfaces;
var Cc = Components.classes;

// gives us an items object:
load("html5sec_items.js");
// possible payloads for within those vectors (items[x].data)
load("html5sec_payloads.js");

// from html5sec.org's import.js:
for (var item in items) {
// replace the payload templates
  for (var payload in payloads) {
    var regex = new RegExp('%' + payload + '%', 'gm');
    items[item].data = items[item].data.replace(regex, payloads[payload]);
    if (items[item].attachment && items[item].attachment.raw) {
      items[item].attachment.raw = items[item].attachment.raw.replace(regex, payloads[payload]);
    }
  }
}
// initialize parser object
var ParserUtils =  Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
var sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly|ParserUtils.SanitizerDropForms|ParserUtils.SanitizerDropNonCSSPresentation;

for (var item in items) {
  // sanitize vector
  var out = ParserUtils.sanitize(items[item].data, sanitizeFlags);
  items[item].sanitized = out;
}

// results for html5sec cheat sheet
var mini_items = items.map(function(e) { return {data:e.data, sanitized:e.sanitized}; });

load("xss_rsnake.js"); // array of rsnake xss cheat sheet entries
rsnake_results = [];
for (var i in xss_rsnake) {
  var out = ParserUtils.sanitize(xss_rsnake[i], sanitizeFlags);
  rsnake_results.push({"data": xss_rsnake[i], "sanitized": out});
}
collected_results = mini_items.concat(rsnake_results);
dump(JSON.stringify(collected_results)); // full output as JSON

// html-strings to stdout:
for (var i of collected_results) {
  dump(i.sanitized);
}

After sanitizing all of these attack vectors, I had to review the results. Since this is my first dive into XPCShell tests, I didn't dare to hook all the logic behind script parsing, image loading, event handler registration and so forth. Instead I reviewed the sanitized output by hand (a JSON capable editor helps a lot). After that I also put the combined output into a single HTML file and opened it in the browser. The Firefox Developer Console helped me confirm that no resources were loaded and no scripts executed.

This means that the sanitizer successfully stripped all the scripts tags, self-submitting forms and event handlers: Security Review done!

For convenience, I have uploaded my test results as a JSON file. It is an array of objects in the format {"data": "...", "sanitized": "..."}.

All posts

  1. Reference Sheet for Principals in Mozilla Code (Mon 03 August 2020)
  2. Hardening Firefox against Injection Attacks – The Technical Details (Tue 07 July 2020)
  3. Understanding Web Security Checks in Firefox (Part 1) (Wed 10 June 2020)
  4. Help Test Firefox's built-in HTML Sanitizer to protect against UXSS bugs (Fri 06 December 2019)
  5. Remote Code Execution in Firefox beyond memory corruptions (Sun 29 September 2019)
  6. XSS in The Digital #ClimateStrike Widget (Mon 23 September 2019)
  7. Chrome switching the XSSAuditor to filter mode re-enables old attack (Fri 10 May 2019)
  8. Challenge Write-up: Subresource Integrity in Service Workers (Sat 25 March 2017)
  9. Finding the SqueezeBox Radio Default SSH Passwort (Fri 02 September 2016)
  10. New CSP directive to make Subresource Integrity mandatory (`require-sri-for`) (Thu 02 June 2016)
  11. Firefox OS apps and beyond (Tue 12 April 2016)
  12. Teacher's Pinboard Write-up (Wed 02 December 2015)
  13. A CDN that can not XSS you: Using Subresource Integrity (Sun 19 July 2015)
  14. The Twitter Gazebo (Sat 18 July 2015)
  15. German Firefox 1.0 ad (OCR) (Sun 09 November 2014)
  16. My thoughts on Tor appliances (Tue 14 October 2014)
  17. Subresource Integrity (Sun 05 October 2014)
  18. Revoke App Permissions on Firefox OS (Sun 24 August 2014)
  19. (Self) XSS at Mozilla's internal Phonebook (Fri 23 May 2014)
  20. Tales of Python's Encoding (Mon 17 March 2014)
  21. On the X-Frame-Options Security Header (Thu 12 December 2013)
  22. html2dom (Tue 24 September 2013)
  23. Security Review: HTML sanitizer in Thunderbird (Mon 22 July 2013)
  24. Week 29 2013 (Sun 21 July 2013)
  25. The First Post (Tue 16 July 2013)