Bug 1432966: Sanitize HTML fragments created for chrome-privileged documents. r=bz f=gijs a=jcristau FENNEC_58_0_1_BUILD1 FENNEC_58_0_1_RELEASE FIREFOX_58_0_1_BUILD1 FIREFOX_58_0_1_RELEASE
authorKris Maglione <maglione.k@gmail.com>
Wed, 24 Jan 2018 14:56:48 -0800
changeset 445698 c2db4a50dc5c93b44852d9a5201f7ec062ecc6cb
parent 445697 331ee799295505824ab78888835846fb69d0d386
child 445699 f33f0b05eaed2a7bc9f82145a4d95b114abae287
push id1638
push userjcristau@mozilla.com
push dateSun, 28 Jan 2018 19:12:52 +0000
treeherdermozilla-release@c2db4a50dc5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jcristau
bugs1432966
milestone58.0.1
Bug 1432966: Sanitize HTML fragments created for chrome-privileged documents. r=bz f=gijs a=jcristau This is a short-term solution to our inability to apply CSP to chrome-privileged documents. Ideally, we should be preventing all inline script execution in chrome-privileged documents, since the reprecussions of XSS in chrome documents are much worse than in content documents. Unfortunately, that's not possible in the near term because a) we don't support CSP in system principal documents at all, and b) we rely heavily on inline JS in our static XUL. This stop-gap solution at least prevents some of the most common vectors of XSS attack, by automatically sanitizing any HTML fragment created for a chrome-privileged document. MozReview-Commit-ID: 5w17celRFr
accessible/tests/mochitest/events/test_mutation.html
browser/base/content/browser-media.js
browser/base/content/test/permissions/browser_reservedkey.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/modules/ExtensionsUI.jsm
browser/modules/webrtcUI.jsm
devtools/client/responsive.html/components/Browser.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_popup.js
devtools/shared/gcli/source/lib/gcli/util/util.js
devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
dom/base/Element.cpp
dom/base/Element.h
dom/base/FragmentOrElement.cpp
dom/base/FragmentOrElement.h
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsDocument.cpp
dom/base/nsIDocument.h
dom/base/test/chrome.ini
dom/base/test/chrome/test_bug683852.xul
dom/base/test/test_fragment_sanitization.xul
dom/webidl/Document.webidl
dom/webidl/Element.webidl
layout/style/test/chrome/bug418986-2.js
mobile/android/chrome/content/config.js
toolkit/content/tests/chrome/test_bug570192.xul
toolkit/mozapps/extensions/content/extensions.js
--- a/accessible/tests/mochitest/events/test_mutation.html
+++ b/accessible/tests/mochitest/events/test_mutation.html
@@ -313,18 +313,18 @@
 
       this.eventSeq = [
         new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.firstChild; }, this.containerNode),
         new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.lastChild; }, this.containerNode),
         new invokerChecker(EVENT_REORDER, this.containerNode)
       ];
 
       this.invoke = function insertReferredElm_invoke() {
-        this.containerNode.innerHTML =
-          "<span id='insertReferredElms_span'></span><input aria-labelledby='insertReferredElms_span'>";
+        this.containerNode.unsafeSetInnerHTML(
+          "<span id='insertReferredElms_span'></span><input aria-labelledby='insertReferredElms_span'>");
       };
 
       this.getID = function insertReferredElm_getID() {
         return "insert inaccessible element and then insert referring element to make it accessible";
       };
     }
 
     function showHiddenParentOfVisibleChild() {
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -29,21 +29,33 @@ var gEMEHandler = {
       return false;
     }
     if (keySystem == "com.widevine.alpha" &&
         Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) {
       return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible");
     }
     return true;
   },
-  getLearnMoreLink(msgId) {
-    let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel");
+  getEMEDisabledFragment(msgId) {
+    let mainMessage = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.message");
+    let [prefix, suffix] = mainMessage.split(/%(?:1\$)?S/).map(s => document.createTextNode(s));
+    let text = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.learnMoreLabel");
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-    return "<label class='text-link' href='" + baseURL + "drm-content'>" +
-           text + "</label>";
+    let link = document.createElement("label");
+    link.className = "text-link";
+    link.setAttribute("href", baseURL + "drm-content");
+    link.textContent = text;
+
+    let fragment = document.createDocumentFragment();
+    [prefix, link, suffix].forEach(n => fragment.appendChild(n));
+    return fragment;
+  },
+  getMessageWithBrandName(notificationId) {
+    let msgId = "emeNotifications." + notificationId + ".message";
+    return gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]);
   },
   receiveMessage({target: browser, data: data}) {
     let parsedData;
     try {
       parsedData = JSON.parse(data);
     } catch (ex) {
       Cu.reportError("Malformed EME video message with data: " + data);
       return;
@@ -51,91 +63,75 @@ var gEMEHandler = {
     let {status: status, keySystem: keySystem} = parsedData;
     // Don't need to show if disabled or keysystem not visible.
     if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) {
       return;
     }
 
     let notificationId;
     let buttonCallback;
-    let params = [];
+    // Notification message can be either a string or a DOM fragment.
+    let notificationMessage;
     switch (status) {
       case "available":
       case "cdm-created":
         // Only show the chain icon for proprietary CDMs. Clearkey is not one.
         if (keySystem != "org.w3.clearkey") {
           this.showPopupNotificationForSuccess(browser, keySystem);
         }
         // ... and bail!
         return;
 
       case "api-disabled":
       case "cdm-disabled":
         notificationId = "drmContentDisabled";
         buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem);
-        params = [this.getLearnMoreLink(notificationId)];
+        notificationMessage = this.getEMEDisabledFragment();
         break;
 
       case "cdm-insufficient-version":
         notificationId = "drmContentCDMInsufficientVersion";
-        params = [this._brandShortName];
+        notificationMessage = this.getMessageWithBrandName(notificationId);
         break;
 
       case "cdm-not-installed":
         notificationId = "drmContentCDMInstalling";
-        params = [this._brandShortName];
+        notificationMessage = this.getMessageWithBrandName(notificationId);
         break;
 
       case "cdm-not-supported":
         // Not to pop up user-level notification because they cannot do anything
         // about it.
         return;
       default:
         Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data));
         return;
     }
 
-    this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback);
-  },
-  showNotificationBar(browser, notificationId, keySystem, labelParams, callback) {
+    // Now actually create the notification
+
     let box = gBrowser.getNotificationBox(browser);
     if (box.getNotificationWithValue(notificationId)) {
       return;
     }
 
-    let msgPrefix = "emeNotifications." + notificationId + ".";
-    let msgId = msgPrefix + "message";
-
-    let message = labelParams.length ?
-                  gNavigatorBundle.getFormattedString(msgId, labelParams) :
-                  gNavigatorBundle.getString(msgId);
-
     let buttons = [];
-    if (callback) {
+    if (buttonCallback) {
+      let msgPrefix = "emeNotifications." + notificationId + ".";
       let btnLabelId = msgPrefix + "button.label";
       let btnAccessKeyId = msgPrefix + "button.accesskey";
       buttons.push({
         label: gNavigatorBundle.getString(btnLabelId),
         accessKey: gNavigatorBundle.getString(btnAccessKeyId),
-        callback
+        callback: buttonCallback,
       });
     }
 
     let iconURL = "chrome://browser/skin/drm-icon.svg";
-
-    // Do a little dance to get rich content into the notification:
-    let fragment = document.createDocumentFragment();
-    let descriptionContainer = document.createElement("description");
-    // eslint-disable-next-line no-unsanitized/property
-    descriptionContainer.innerHTML = message;
-    while (descriptionContainer.childNodes.length) {
-      fragment.appendChild(descriptionContainer.childNodes[0]);
-    }
-
-    box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
+    box.appendNotification(notificationMessage, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
                            buttons);
   },
   showPopupNotificationForSuccess(browser, keySystem) {
     // We're playing EME content! Remove any "we can't play because..." messages.
     var box = gBrowser.getNotificationBox(browser);
     ["drmContentDisabled",
      "drmContentCDMInstalling"
      ].forEach(function(value) {
--- a/browser/base/content/test/permissions/browser_reservedkey.js
+++ b/browser/base/content/test/permissions/browser_reservedkey.js
@@ -5,17 +5,17 @@ add_task(async function test_reserved_sh
                        oncommand='this.setAttribute("count", Number(this.getAttribute("count")) + 1)'/>
                   <key id='kt_notreserved' modifiers='shift' key='P' reserved='false' count='0'
                        oncommand='this.setAttribute("count", Number(this.getAttribute("count")) + 1)'/>
                   <key id='kt_reserveddefault' modifiers='shift' key='Q' count='0'
                        oncommand='this.setAttribute("count", Number(this.getAttribute("count")) + 1)'/>
                 </keyset>`;
 
   let container = document.createElement("box");
-  container.innerHTML = keyset;
+  container.unsafeSetInnerHTML(keyset);
   document.documentElement.appendChild(container);
   /* eslint-enable no-unsanitized/property */
 
   const pageUrl = "data:text/html,<body onload='document.body.firstChild.focus();'><div onkeydown='event.preventDefault();' tabindex=0>Test</div></body>";
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
   EventUtils.synthesizeKey("O", { shiftKey: true });
   EventUtils.synthesizeKey("P", { shiftKey: true });
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -279,17 +279,17 @@ const CustomizableWidgets = [
         link.setAttribute("mobile-promo-os", os);
         link.className = "text-link remotetabs-promo-link";
         return link.outerHTML;
       });
       let promoParentElt = doc.getElementById("PanelUI-remotetabs-mobile-promo");
       // Put it all together...
       let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs);
       // eslint-disable-next-line no-unsanitized/property
-      promoParentElt.innerHTML = contents;
+      promoParentElt.unsafeSetInnerHTML(contents);
       // We manually manage the "click" event to open the promo links because
       // allowing the "text-link" widget handle it has 2 problems: (1) it only
       // supports button 0 and (2) it's tricky to intercept when it does the
       // open and auto-close the panel. (1) can probably be fixed, but (2) is
       // trickier without hard-coding here the knowledge of exactly what buttons
       // it does support.
       // So we allow left and middle clicks to open the link in a new tab and
       // close the panel; not setting a "href" attribute prevents the text-link
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -424,20 +424,20 @@ this.ExtensionsUI = {
         hideClose: true,
         timeout: Date.now() + 30000,
         popupIconURL: icon,
         eventCallback(topic) {
           if (topic == "showing") {
             let doc = this.browser.ownerDocument;
         // eslint-disable-next-line no-unsanitized/property
             doc.getElementById("addon-installed-notification-header")
-               .innerHTML = msg1;
+               .unsafeSetInnerHTML(msg1);
             // eslint-disable-next-line no-unsanitized/property
             doc.getElementById("addon-installed-notification-message")
-               .innerHTML = msg2;
+               .unsafeSetInnerHTML(msg2);
           } else if (topic == "dismissed") {
             resolve();
           }
         }
       };
 
       popups.show(target, "addon-installed", "", "addons-notification-icon",
                   action, null, options);
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -634,31 +634,36 @@ function prompt(aBrowser, aRequest) {
             warning.hidden = false;
             let string;
             let bundle = chromeWin.gNavigatorBundle;
 
             let learnMoreText =
               bundle.getString("getUserMedia.shareScreen.learnMoreLabel");
             let baseURL =
               Services.urlFormatter.formatURLPref("app.support.baseURL");
-            let learnMore =
-              "<label class='text-link' href='" + baseURL + "screenshare-safety'>" +
-              learnMoreText + "</label>";
+
+            let learnMore = chromeWin.document.createElement("label");
+            learnMore.className = "text-link";
+            learnMore.setAttribute("href", baseURL + "screenshare-safety");
+            learnMore.textContent = learnMoreText;
 
             if (type == "screen") {
               string = bundle.getFormattedString("getUserMedia.shareScreenWarning.message",
-                                                 [learnMore]);
+                                                 ["<>"]);
             } else {
               let brand =
                 doc.getElementById("bundle_brand").getString("brandShortName");
               string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message",
-                                                 [brand, learnMore]);
+                                                 [brand, "<>"]);
             }
-            // eslint-disable-next-line no-unsanitized/property
-            warning.innerHTML = string;
+
+            let [pre, post] = string.split("<>");
+            warning.textContent = pre;
+            warning.appendChild(learnMore);
+            warning.appendChild(chromeWin.document.createTextNode(post));
           }
 
           let perms = Services.perms;
           let chromeUri = Services.io.newURI(doc.documentURI);
           perms.add(chromeUri, "MediaManagerVideo", perms.ALLOW_ACTION,
                     perms.EXPIRE_SESSION);
 
           video.deviceId = deviceId;
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -13,16 +13,22 @@ const { DOM: dom, createClass, addons, P
   require("devtools/client/shared/vendor/react");
 
 const e10s = require("../utils/e10s");
 const message = require("../utils/message");
 const { getToplevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
+// Allow creation of HTML fragments without automatic sanitization, even
+// though we're in a chrome-privileged document.
+// This is, unfortunately, necessary in order to React to function
+// correctly.
+document.allowUnsafeHTML = true;
+
 module.exports = createClass({
 
   /**
    * This component is not allowed to depend directly on frequently changing
    * data (width, height) due to the use of `dangerouslySetInnerHTML` below.
    * Any changes in props will cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
    */
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_popup.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_popup.js
@@ -92,19 +92,23 @@ add_task(async function () {
   is(popup.itemCount, 0, "items cleared");
   ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
 
   const onPopupClose = popup.once("popup-closed");
   popup.hidePopup();
   await onPopupClose;
 });
 
+function stripNS(text) {
+  return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
+}
+
 function checkActiveDescendant(popup, input) {
   let activeElement = input.ownerDocument.activeElement;
   let descendantId = activeElement.getAttribute("aria-activedescendant");
   let popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
   let cloneItem = input.ownerDocument.querySelector("#" + descendantId);
 
   ok(popupItem, "Active descendant is found in the popup list");
   ok(cloneItem, "Active descendant is found in the list clone");
-  is(popupItem.outerHTML, cloneItem.outerHTML,
+  is(stripNS(popupItem.outerHTML), cloneItem.outerHTML,
     "Cloned item has the same HTML as the original element");
 }
--- a/devtools/shared/gcli/source/lib/gcli/util/util.js
+++ b/devtools/shared/gcli/source/lib/gcli/util/util.js
@@ -493,17 +493,21 @@ exports.setTextContent = function(elem, 
  */
 exports.setContents = function(elem, contents) {
   if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) {
     exports.clearElement(elem);
     elem.appendChild(contents);
     return;
   }
 
-  if ('innerHTML' in elem) {
+  if ('unsafeSetInnerHTML' in elem) {
+    // FIXME: Stop relying on unsanitized HTML.
+    elem.unsafeSetInnerHTML(contents);
+  }
+  else if ('innerHTML' in elem) {
     elem.innerHTML = contents;
   }
   else {
     try {
       var ns = elem.ownerDocument.documentElement.namespaceURI;
       if (!ns) {
         ns = exports.NS_XHTML;
       }
--- a/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
+++ b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js
@@ -13,31 +13,31 @@ add_task(function* () {
   let TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
   let str1 = STARTUP_L10N.getStr("inspector.label");
   let str2 = STARTUP_L10N.getStr("inspector.accesskey");
   let str3 = TOOLBOX_L10N.getStr("toolbox.defaultTitle");
   ok(str1 && str2 && str3, "If this failed, strings should be updated in the test");
 
   info("Create the test markup");
   let div = document.createElement("div");
-  div.innerHTML =
+  div.unsafeSetInnerHTML(
   `<div data-localization-bundle="devtools/client/locales/startup.properties">
      <div id="d0" data-localization="content=inspector.someInvalidKey"></div>
      <div id="d1" data-localization="content=inspector.label">Text will disappear</div>
      <div id="d2" data-localization="content=inspector.label;title=inspector.accesskey">
      </div>
      <!-- keep the following data-localization on two separate lines -->
      <div id="d3" data-localization="content=inspector.label;
                                      title=inspector.accesskey"></div>
      <div id="d4" data-localization="aria-label=inspector.label">Some content</div>
      <div data-localization-bundle="devtools/client/locales/toolbox.properties">
        <div id="d5" data-localization="content=toolbox.defaultTitle"></div>
      </div>
    </div>
-  `;
+  `);
 
   info("Use localization helper to localize the test markup");
   localizeMarkup(div);
 
   let div1 = div.querySelector("#d1");
   let div2 = div.querySelector("#d2");
   let div3 = div.querySelector("#d3");
   let div4 = div.querySelector("#d4");
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -3939,16 +3939,22 @@ Element::GetInnerHTML(nsAString& aInnerH
 
 void
 Element::SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError)
 {
   SetInnerHTMLInternal(aInnerHTML, aError);
 }
 
 void
+Element::UnsafeSetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError)
+{
+  SetInnerHTMLInternal(aInnerHTML, aError, true);
+}
+
+void
 Element::GetOuterHTML(nsAString& aOuterHTML)
 {
   GetMarkup(true, aOuterHTML);
 }
 
 void
 Element::SetOuterHTML(const nsAString& aOuterHTML, ErrorResult& aError)
 {
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1194,16 +1194,17 @@ public:
   void GetAnimations(const AnimationFilter& filter,
                      nsTArray<RefPtr<Animation>>& aAnimations);
   static void GetAnimationsUnsorted(Element* aElement,
                                     CSSPseudoElementType aPseudoType,
                                     nsTArray<RefPtr<Animation>>& aAnimations);
 
   NS_IMETHOD GetInnerHTML(nsAString& aInnerHTML);
   virtual void SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError);
+  void UnsafeSetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError);
   void GetOuterHTML(nsAString& aOuterHTML);
   void SetOuterHTML(const nsAString& aOuterHTML, ErrorResult& aError);
   void InsertAdjacentHTML(const nsAString& aPosition, const nsAString& aText,
                           ErrorResult& aError);
 
   //----------------------------------------
 
   /**
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -2467,17 +2467,18 @@ ContainsMarkup(const nsAString& aStr)
     }
     ++start;
   }
 
   return false;
 }
 
 void
-FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError)
+FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError,
+                                        bool aNeverSanitize)
 {
   FragmentOrElement* target = this;
   // Handle template case.
   if (nsNodeUtils::IsTemplateElement(target)) {
     DocumentFragment* frag =
       static_cast<HTMLTemplateElement*>(target)->Content();
     MOZ_ASSERT(frag);
     target = frag;
@@ -2521,32 +2522,38 @@ FragmentOrElement::SetInnerHTMLInternal(
 
   ShadowRoot* shadowRoot = ShadowRoot::FromNode(this);
   if (shadowRoot) {
     // Fix up the context to be the host of the ShadowRoot.
     contextLocalName = shadowRoot->GetHost()->NodeInfo()->NameAtom();
     contextNameSpaceID = shadowRoot->GetHost()->GetNameSpaceID();
   }
 
+  auto sanitize = (aNeverSanitize ? nsContentUtils::NeverSanitize
+                                  : nsContentUtils::SanitizeSystemPrivileged);
+
   if (doc->IsHTMLDocument()) {
     int32_t oldChildCount = target->GetChildCount();
     aError = nsContentUtils::ParseFragmentHTML(aInnerHTML,
                                                target,
                                                contextLocalName,
                                                contextNameSpaceID,
                                                doc->GetCompatibilityMode() ==
                                                  eCompatibility_NavQuirks,
-                                               true);
+                                               true,
+                                               sanitize);
     mb.NodesAdded();
     // HTML5 parser has notified, but not fired mutation events.
     nsContentUtils::FireMutationEventsForDirectParsing(doc, target,
                                                        oldChildCount);
   } else {
     RefPtr<DocumentFragment> df =
-      nsContentUtils::CreateContextualFragment(target, aInnerHTML, true, aError);
+      nsContentUtils::CreateContextualFragment(target, aInnerHTML, true,
+                                               sanitize,
+                                               aError);
     if (!aError.Failed()) {
       // Suppress assertion about node removal mutation events that can't have
       // listeners anyway, because no one has had the chance to register mutation
       // listeners on the fragment that comes from the parser.
       nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
 
       static_cast<nsINode*>(target)->AppendChild(*df, aError);
       mb.NodesAdded();
--- a/dom/base/FragmentOrElement.h
+++ b/dom/base/FragmentOrElement.h
@@ -366,17 +366,18 @@ public:
      */
     RefPtr<nsDOMTokenList> mClassList;
 
     mozilla::UniquePtr<nsExtendedDOMSlots> mExtendedSlots;
   };
 
 protected:
   void GetMarkup(bool aIncludeSelf, nsAString& aMarkup);
-  void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError);
+  void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError,
+                            bool aNeverSanitize = false);
 
   // Override from nsINode
   virtual nsINode::nsSlots* CreateSlots() override;
 
   nsDOMSlots *DOMSlots()
   {
     return static_cast<nsDOMSlots*>(Slots());
   }
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -158,16 +158,17 @@
 #include "nsIMIMEService.h"
 #include "nsINode.h"
 #include "mozilla/dom/NodeInfo.h"
 #include "nsIObjectLoadingContent.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
 #include "nsIOfflineCacheUpdate.h"
 #include "nsIParser.h"
+#include "nsIParserUtils.h"
 #include "nsIPermissionManager.h"
 #include "nsIPluginHost.h"
 #include "nsIRemoteBrowser.h"
 #include "nsIRequest.h"
 #include "nsIRunnable.h"
 #include "nsIScriptContext.h"
 #include "nsIScriptError.h"
 #include "nsIScriptGlobalObject.h"
@@ -198,16 +199,17 @@
 #include "nsSandboxFlags.h"
 #include "nsScriptSecurityManager.h"
 #include "nsSerializationHelper.h"
 #include "nsStreamUtils.h"
 #include "nsTextEditorState.h"
 #include "nsTextFragment.h"
 #include "nsTextNode.h"
 #include "nsThreadUtils.h"
+#include "nsTreeSanitizer.h"
 #include "nsUnicodeProperties.h"
 #include "nsURLHelper.h"
 #include "nsViewManager.h"
 #include "nsViewportInfo.h"
 #include "nsWidgetsCID.h"
 #include "nsIWindowProvider.h"
 #include "nsWrapperCacheInlines.h"
 #include "nsXULPopupManager.h"
@@ -5055,16 +5057,17 @@ nsContentUtils::CreateContextualFragment
                                       aPreventScriptExecution, rv).take();
   return rv.StealNSResult();
 }
 
 already_AddRefed<DocumentFragment>
 nsContentUtils::CreateContextualFragment(nsINode* aContextNode,
                                          const nsAString& aFragment,
                                          bool aPreventScriptExecution,
+                                         SanitizeFragments aSanitize,
                                          ErrorResult& aRv)
 {
   if (!aContextNode) {
     aRv.Throw(NS_ERROR_INVALID_ARG);
     return nullptr;
   }
 
   // If we don't have a document here, we can't get the right security context
@@ -5090,24 +5093,26 @@ nsContentUtils::CreateContextualFragment
     }
 
     if (contextAsContent && !contextAsContent->IsHTMLElement(nsGkAtoms::html)) {
       aRv = ParseFragmentHTML(aFragment, frag,
                               contextAsContent->NodeInfo()->NameAtom(),
                               contextAsContent->GetNameSpaceID(),
                               (document->GetCompatibilityMode() ==
                                eCompatibility_NavQuirks),
-                              aPreventScriptExecution);
+                              aPreventScriptExecution,
+                              aSanitize);
     } else {
       aRv = ParseFragmentHTML(aFragment, frag,
                               nsGkAtoms::body,
                               kNameSpaceID_XHTML,
                               (document->GetCompatibilityMode() ==
                                eCompatibility_NavQuirks),
-                              aPreventScriptExecution);
+                              aPreventScriptExecution,
+                              aSanitize);
     }
 
     return frag.forget();
   }
 
   AutoTArray<nsString, 32> tagStack;
   nsAutoString uriStr, nameStr;
   nsCOMPtr<nsIContent> content = do_QueryInterface(aContextNode);
@@ -5161,17 +5166,18 @@ nsContentUtils::CreateContextualFragment
       }
     }
 
     content = content->GetParent();
   }
 
   nsCOMPtr<nsIDOMDocumentFragment> frag;
   aRv = ParseFragmentXML(aFragment, document, tagStack,
-                         aPreventScriptExecution, getter_AddRefs(frag));
+                         aPreventScriptExecution, getter_AddRefs(frag),
+                         aSanitize);
   return frag.forget().downcast<DocumentFragment>();
 }
 
 /* static */
 void
 nsContentUtils::DropFragmentParsers()
 {
   NS_IF_RELEASE(sHTMLFragmentParser);
@@ -5188,37 +5194,64 @@ nsContentUtils::XPCOMShutdown()
 
 /* static */
 nsresult
 nsContentUtils::ParseFragmentHTML(const nsAString& aSourceBuffer,
                                   nsIContent* aTargetNode,
                                   nsAtom* aContextLocalName,
                                   int32_t aContextNamespace,
                                   bool aQuirks,
-                                  bool aPreventScriptExecution)
+                                  bool aPreventScriptExecution,
+                                  SanitizeFragments aSanitize)
 {
   AutoTimelineMarker m(aTargetNode->OwnerDoc()->GetDocShell(), "Parse HTML");
 
   if (nsContentUtils::sFragmentParsingActive) {
     NS_NOTREACHED("Re-entrant fragment parsing attempted.");
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
   mozilla::AutoRestore<bool> guard(nsContentUtils::sFragmentParsingActive);
   nsContentUtils::sFragmentParsingActive = true;
   if (!sHTMLFragmentParser) {
     NS_ADDREF(sHTMLFragmentParser = new nsHtml5StringParser());
     // Now sHTMLFragmentParser owns the object
   }
+
+  nsIContent* target = aTargetNode;
+
+  // If this is a chrome-privileged document, create a fragment first, and
+  // sanitize it before insertion.
+  RefPtr<DocumentFragment> fragment;
+  if (aSanitize != NeverSanitize && !aTargetNode->OwnerDoc()->AllowUnsafeHTML()) {
+    fragment = new DocumentFragment(aTargetNode->OwnerDoc()->NodeInfoManager());
+    target = fragment;
+  }
+
   nsresult rv =
     sHTMLFragmentParser->ParseFragment(aSourceBuffer,
-                                       aTargetNode,
+                                       target,
                                        aContextLocalName,
                                        aContextNamespace,
                                        aQuirks,
                                        aPreventScriptExecution);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (fragment) {
+    // Don't fire mutation events for nodes removed by the sanitizer.
+    nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
+
+    nsTreeSanitizer sanitizer(nsIParserUtils::SanitizerAllowStyle |
+                              nsIParserUtils::SanitizerAllowComments);
+    sanitizer.Sanitize(fragment);
+
+    ErrorResult error;
+    aTargetNode->AppendChild(*fragment, error);
+    rv = error.StealNSResult();
+  }
+
   return rv;
 }
 
 /* static */
 nsresult
 nsContentUtils::ParseDocumentHTML(const nsAString& aSourceBuffer,
                                   nsIDocument* aTargetDocument,
                                   bool aScriptingEnabledForNoscriptParsing)
@@ -5243,17 +5276,18 @@ nsContentUtils::ParseDocumentHTML(const 
 }
 
 /* static */
 nsresult
 nsContentUtils::ParseFragmentXML(const nsAString& aSourceBuffer,
                                  nsIDocument* aDocument,
                                  nsTArray<nsString>& aTagStack,
                                  bool aPreventScriptExecution,
-                                 nsIDOMDocumentFragment** aReturn)
+                                 nsIDOMDocumentFragment** aReturn,
+                                 SanitizeFragments aSanitize)
 {
   AutoTimelineMarker m(aDocument->GetDocShell(), "Parse XML");
 
   if (nsContentUtils::sFragmentParsingActive) {
     NS_NOTREACHED("Re-entrant fragment parsing attempted.");
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
   mozilla::AutoRestore<bool> guard(nsContentUtils::sFragmentParsingActive);
@@ -5282,16 +5316,30 @@ nsContentUtils::ParseFragmentXML(const n
     NS_IF_RELEASE(sXMLFragmentParser);
     NS_IF_RELEASE(sXMLFragmentSink);
     return rv;
   }
 
   rv = sXMLFragmentSink->FinishFragmentParsing(aReturn);
 
   sXMLFragmentParser->Reset();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // If this is a chrome-privileged document, sanitize the fragment before
+  // returning.
+  if (aSanitize != NeverSanitize && !aDocument->AllowUnsafeHTML()) {
+    // Don't fire mutation events for nodes removed by the sanitizer.
+    nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
+
+    RefPtr<DocumentFragment> fragment = static_cast<DocumentFragment*>(*aReturn);
+
+    nsTreeSanitizer sanitizer(nsIParserUtils::SanitizerAllowStyle |
+                              nsIParserUtils::SanitizerAllowComments);
+    sanitizer.Sanitize(fragment);
+  }
 
   return rv;
 }
 
 /* static */
 nsresult
 nsContentUtils::ConvertToPlainText(const nsAString& aSourceBuffer,
                                    nsAString& aResultBuffer,
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1562,77 +1562,99 @@ public:
    *
    * @param aLocalname localname of the node
    * @param aPrefix prefix of the node
    * @param aNamespaceID namespace of the node
    */
   static bool IsValidNodeName(nsAtom *aLocalName, nsAtom *aPrefix,
                                 int32_t aNamespaceID);
 
+  enum SanitizeFragments {
+    SanitizeSystemPrivileged,
+    NeverSanitize,
+  };
+
   /**
    * Creates a DocumentFragment from text using a context node to resolve
    * namespaces.
    *
    * Note! In the HTML case with the HTML5 parser enabled, this is only called
    * from Range.createContextualFragment() and the implementation here is
    * quirky accordingly (html context node behaves like a body context node).
    * If you don't want that quirky behavior, don't use this method as-is!
    *
    * @param aContextNode the node which is used to resolve namespaces
    * @param aFragment the string which is parsed to a DocumentFragment
    * @param aReturn the resulting fragment
    * @param aPreventScriptExecution whether to mark scripts as already started
+   * @param aSanitize whether the fragment should be sanitized prior to
+   *        injection
    */
   static nsresult CreateContextualFragment(nsINode* aContextNode,
                                            const nsAString& aFragment,
                                            bool aPreventScriptExecution,
                                            nsIDOMDocumentFragment** aReturn);
   static already_AddRefed<mozilla::dom::DocumentFragment>
   CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment,
                            bool aPreventScriptExecution,
+                           SanitizeFragments aSanitize,
                            mozilla::ErrorResult& aRv);
+  static already_AddRefed<mozilla::dom::DocumentFragment>
+  CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment,
+                           bool aPreventScriptExecution,
+                           mozilla::ErrorResult& aRv)
+  {
+    return CreateContextualFragment(aContextNode, aFragment, aPreventScriptExecution,
+                                    SanitizeSystemPrivileged, aRv);
+  }
 
   /**
    * Invoke the fragment parsing algorithm (innerHTML) using the HTML parser.
    *
    * @param aSourceBuffer the string being set as innerHTML
    * @param aTargetNode the target container
    * @param aContextLocalName local name of context node
    * @param aContextNamespace namespace of context node
    * @param aQuirks true to make <table> not close <p>
    * @param aPreventScriptExecution true to prevent scripts from executing;
    *        don't set to false when parsing into a target node that has been
    *        bound to tree.
+   * @param aSanitize whether the fragment should be sanitized prior to
+   *        injection
    * @return NS_ERROR_DOM_INVALID_STATE_ERR if a re-entrant attempt to parse
    *         fragments is made, NS_ERROR_OUT_OF_MEMORY if aSourceBuffer is too
    *         long and NS_OK otherwise.
    */
   static nsresult ParseFragmentHTML(const nsAString& aSourceBuffer,
                                     nsIContent* aTargetNode,
                                     nsAtom* aContextLocalName,
                                     int32_t aContextNamespace,
                                     bool aQuirks,
-                                    bool aPreventScriptExecution);
+                                    bool aPreventScriptExecution,
+                                    SanitizeFragments aSanitize = SanitizeSystemPrivileged);
 
   /**
    * Invoke the fragment parsing algorithm (innerHTML) using the XML parser.
    *
    * @param aSourceBuffer the string being set as innerHTML
    * @param aTargetNode the target container
    * @param aTagStack the namespace mapping context
    * @param aPreventExecution whether to mark scripts as already started
    * @param aReturn the result fragment
+   * @param aSanitize whether the fragment should be sanitized prior to
+   *        injection
    * @return NS_ERROR_DOM_INVALID_STATE_ERR if a re-entrant attempt to parse
    *         fragments is made, a return code from the XML parser.
    */
   static nsresult ParseFragmentXML(const nsAString& aSourceBuffer,
                                    nsIDocument* aDocument,
                                    nsTArray<nsString>& aTagStack,
                                    bool aPreventScriptExecution,
-                                   nsIDOMDocumentFragment** aReturn);
+                                   nsIDOMDocumentFragment** aReturn,
+                                   SanitizeFragments aSanitize = SanitizeSystemPrivileged);
 
   /**
    * Parse a string into a document using the HTML parser.
    * Script elements are marked unexecutable.
    *
    * @param aSourceBuffer the string to parse as an HTML document
    * @param aTargetDocument the document object to parse into. Must not have
    *                        child nodes.
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1559,16 +1559,17 @@ nsIDocument::nsIDocument()
     mHasScrollLinkedEffect(false),
     mFrameRequestCallbacksScheduled(false),
     mIsTopLevelContentDocument(false),
     mIsContentDocument(false),
     mMightHaveStaleServoData(false),
     mDidCallBeginLoad(false),
     mBufferingCSPViolations(false),
     mAllowPaymentRequest(false),
+    mAllowUnsafeHTML(false),
     mIsScopedStyleEnabled(eScopedStyle_Unknown),
     mCompatMode(eCompatibility_FullStandards),
     mReadyState(ReadyState::READYSTATE_UNINITIALIZED),
     mStyleBackendType(StyleBackendType::None),
 #ifdef MOZILLA_INTERNAL_API
     mVisibilityState(dom::VisibilityState::Hidden),
 #else
     mDummy(0),
@@ -6482,16 +6483,23 @@ nsDocument::CustomElementConstructor(JSC
 
   // The prototype setup happens in Element::WrapObject().
   nsresult rv = nsContentUtils::WrapNative(aCx, element, element, args.rval());
   NS_ENSURE_SUCCESS(rv, true);
 
   return true;
 }
 
+bool
+nsIDocument::AllowUnsafeHTML() const
+{
+  return (!nsContentUtils::IsSystemPrincipal(NodePrincipal()) ||
+          mAllowUnsafeHTML);
+}
+
 void
 nsDocument::ScheduleSVGForPresAttrEvaluation(nsSVGElement* aSVG)
 {
   mLazySVGPresElements.PutEntry(aSVG);
 }
 
 void
 nsDocument::UnscheduleSVGForPresAttrEvaluation(nsSVGElement* aSVG)
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2926,16 +2926,18 @@ public:
   already_AddRefed<mozilla::dom::CDATASection>
     CreateCDATASection(const nsAString& aData, mozilla::ErrorResult& rv);
   already_AddRefed<mozilla::dom::Attr>
     CreateAttribute(const nsAString& aName, mozilla::ErrorResult& rv);
   already_AddRefed<mozilla::dom::Attr>
     CreateAttributeNS(const nsAString& aNamespaceURI,
                       const nsAString& aQualifiedName,
                       mozilla::ErrorResult& rv);
+  void SetAllowUnsafeHTML(bool aAllow) { mAllowUnsafeHTML = aAllow; }
+  bool AllowUnsafeHTML() const;
   void GetInputEncoding(nsAString& aInputEncoding) const;
   already_AddRefed<mozilla::dom::Location> GetLocation() const;
   void GetReferrer(nsAString& aReferrer) const;
   void GetLastModified(nsAString& aLastModified) const;
   void GetReadyState(nsAString& aReadyState) const;
   // Not const because otherwise the compiler can't figure out whether to call
   // this GetTitle or the nsAString version from non-const methods, since
   // neither is an exact match.
@@ -3590,16 +3592,20 @@ protected:
 
   // True if any CSP violation reports for this doucment will be buffered in
   // mBufferedCSPViolations instead of being sent immediately.
   bool mBufferingCSPViolations : 1;
 
   // True if the document is allowed to use PaymentRequest.
   bool mAllowPaymentRequest : 1;
 
+  // True if unsafe HTML fragments should be allowed in chrome-privileged
+  // documents.
+  bool mAllowUnsafeHTML : 1;
+
   // Whether <style scoped> support is enabled in this document.
   enum { eScopedStyle_Unknown, eScopedStyle_Disabled, eScopedStyle_Enabled };
   unsigned int mIsScopedStyleEnabled : 2;
 
   // Compatibility mode
   nsCompatibility mCompatMode;
 
   // Our readyState
--- a/dom/base/test/chrome.ini
+++ b/dom/base/test/chrome.ini
@@ -20,16 +20,17 @@ support-files =
 [test_bug715041.xul]
 [test_bug715041_removal.xul]
 [test_bug945152.html]
 [test_bug1008126.html]
 [test_bug1016960.html]
 [test_copypaste.xul]
 subsuite = clipboard
 [test_domrequesthelper.xul]
+[test_fragment_sanitization.xul]
 [test_messagemanager_principal.html]
 [test_messagemanager_send_principal.html]
 skip-if = buildapp == 'mulet'
 [test_mozbrowser_apis_allowed.html]
 [test_navigator_resolve_identity_xrays.xul]
 support-files = file_navigator_resolve_identity_xrays.xul
 [test_sandboxed_blob_uri.html]
 [test_sendQueryContentAndSelectionSetEvent.html]
--- a/dom/base/test/chrome/test_bug683852.xul
+++ b/dom/base/test/chrome/test_bug683852.xul
@@ -15,16 +15,18 @@ https://bugzilla.mozilla.org/show_bug.cg
   </body>
 
   <!-- test code goes here -->
   <script type="application/javascript">
   <![CDATA[
   /** Test for Bug 683852 **/
   SimpleTest.waitForExplicitFinish();
 
+  const NS_HTML = "http://www.w3.org/1999/xhtml";
+
   function startTest() {
     is(document.contains(document), true, "Document should contain itself!");
 
     var tb = document.getElementById("testbutton");
     is(document.contains(tb), true, "Document should contain element in it!");
     is(tb.contains(tb), true, "Element should contain itself.")
     var anon = document.getAnonymousElementByAttribute(tb, "anonid", "button-box");
     is(document.contains(anon), false, "Document should not contain anonymous element in it!");
@@ -43,17 +45,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
     is(document.contains(null), false, "Document shouldn't contain null.");
 
     var pi = document.createProcessingInstruction("adf", "asd");
     is(pi.contains(document), false, "Processing instruction shouldn't contain document");
     document.documentElement.appendChild(pi);
     document.contains(pi, true, "Document should contain processing instruction");
 
-    var df = document.createRange().createContextualFragment("<div>foo</div>");
+    var df = document.createRange().createContextualFragment(`<div xmlns="${NS_HTML}">foo</div>`);
     is(df.contains(df.firstChild), true, "Document fragment should contain its child");
     is(df.contains(df.firstChild.firstChild), true,
        "Document fragment should contain its descendant");
     is(df.contains(df), true, "Document fragment should contain itself.");
 
     var d = document.implementation.createHTMLDocument("");
     is(document.contains(d), false,
        "Document shouldn't contain another document.");
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_fragment_sanitization.xul
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1432966
+-->
+<window title="Mozilla Bug 1432966"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"/>
+
+  <script type="application/javascript"><![CDATA[
+
+var { classes: Cc, interfaces: Ci } = Components;
+
+const NS_HTML = "http://www.w3.org/1999/xhtml";
+
+function awaitLoad(frame) {
+  return new Promise(resolve => {
+    frame.addEventListener("load", resolve, {once: true});
+  });
+}
+
+async function testFrame(frame, html, expected = html) {
+  document.querySelector("body").appendChild(frame);
+  await awaitLoad(frame);
+
+  // Remove the xmlns attributes that will be automatically added when we're
+  // in an XML document, and break the comparison.
+  function unNS(text) {
+    return text.replace(RegExp(` xmlns="${NS_HTML}"`, "g"), "");
+  }
+
+  let doc = frame.contentDocument;
+  let body = doc.body || doc.documentElement;
+
+  let div = doc.createElementNS(NS_HTML, "div");
+  body.appendChild(div);
+
+  div.innerHTML = html;
+  is(unNS(div.innerHTML), expected, "innerHTML value");
+
+  div.innerHTML = "<div></div>";
+  div.firstChild.outerHTML = html;
+  is(unNS(div.innerHTML), expected, "outerHTML value");
+
+  div.textContent = "";
+  div.insertAdjacentHTML("beforeend", html);
+  is(unNS(div.innerHTML), expected, "insertAdjacentHTML('beforeend') value");
+
+  div.innerHTML = "<a>foo</a>";
+  div.firstChild.insertAdjacentHTML("afterend", html);
+  is(unNS(div.innerHTML), "<a>foo</a>" + expected, "insertAdjacentHTML('afterend') value");
+
+  frame.remove();
+}
+
+add_task(async function test_fragment_sanitization() {
+  const XUL_URL = "chrome://global/content/win.xul";
+  const HTML_URL = "chrome://mochitests/content/chrome/dom/base/test/file_empty.html";
+
+  const HTML = '<a onclick="foo()" href="javascript:foo"><script>bar()<\/script>Meh.</a><a href="http://foo/"></a>';
+  const SANITIZED = '<a>Meh.</a><a href="http://foo/"></a>';
+
+  info("Test content HTML document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = "http://example.com/";
+
+    await testFrame(frame, HTML);
+  }
+
+  info("Test chrome HTML document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = HTML_URL;
+
+    await testFrame(frame, HTML, SANITIZED);
+  }
+
+  info("Test chrome XUL document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = XUL_URL;
+
+    await testFrame(frame, HTML, SANITIZED);
+  }
+});
+
+  ]]></script>
+
+  <description style="-moz-user-focus: normal; -moz-user-select: text;"><![CDATA[
+    hello
+    world
+  ]]></description>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432966"
+       target="_blank">Mozilla Bug 1432966</a>
+  </body>
+</window>
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -93,16 +93,21 @@ interface Document : Node {
   // These are not in the spec, but leave them for now for backwards compat.
   // So sort of like Gecko extensions
   [NewObject, Throws]
   CDATASection createCDATASection(DOMString data);
   [NewObject, Throws]
   Attr createAttribute(DOMString name);
   [NewObject, Throws]
   Attr createAttributeNS(DOMString? namespace, DOMString name);
+
+  // Allows setting innerHTML without automatic sanitization.
+  // Do not use this.
+  [ChromeOnly]
+  attribute boolean allowUnsafeHTML;
 };
 
 // http://www.whatwg.org/specs/web-apps/current-work/#the-document-object
 partial interface Document {
   [PutForwards=href, Unforgeable] readonly attribute Location? location;
   //(HTML only)         attribute DOMString domain;
   readonly attribute DOMString referrer;
   //(HTML only)         attribute DOMString cookie;
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -220,16 +220,26 @@ partial interface Element {
 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
 partial interface Element {
   [CEReactions, Pure,SetterThrows,TreatNullAs=EmptyString]
   attribute DOMString innerHTML;
   [CEReactions, Pure,SetterThrows,TreatNullAs=EmptyString]
   attribute DOMString outerHTML;
   [CEReactions, Throws]
   void insertAdjacentHTML(DOMString position, DOMString text);
+
+  /**
+   * Like the innerHTML setter, but does not sanitize its values, even in
+   * chrome-privileged documents.
+   *
+   * If you're thinking about using this, don't. You have many, much better
+   * options.
+   */
+  [ChromeOnly, Throws]
+  void unsafeSetInnerHTML(DOMString html);
 };
 
 // http://www.w3.org/TR/selectors-api/#interface-definitions
 partial interface Element {
   [Throws, Pure]
   Element?  querySelector(DOMString selectors);
   [Throws, Pure]
   NodeList  querySelectorAll(DOMString selectors);
--- a/layout/style/test/chrome/bug418986-2.js
+++ b/layout/style/test/chrome/bug418986-2.js
@@ -229,23 +229,33 @@ var generateCSSLines = function (resisti
 // __green__.
 // Returns the computed color style corresponding to green.
 var green = (function () {
   let temp = document.createElement("span");
   temp.style.backgroundColor = "green";
   return getComputedStyle(temp).backgroundColor;
 })();
 
+// Injected HTML will automatically be sanitized when we're in a chrome
+// document unless we use `unsafeSetInnerHTML`. That function doesn't
+// exist in non-chrome documents, so add a stub to allow the same code
+// to run in both.
+if (!Element.prototype.unsafeSetInnerHTML) {
+  Element.prototype.unsafeSetInnerHTML = html => {
+    this.innerHTML = html;
+  };
+}
+
 // __testCSS(resisting)__.
 // Creates a series of divs and CSS using media queries to set their
 // background color. If all media queries match as expected, then
 // all divs should have a green background color.
 var testCSS = function (resisting) {
-  document.getElementById("display").innerHTML = generateHtmlLines(resisting);
-  document.getElementById("test-css").innerHTML = generateCSSLines(resisting);
+  document.getElementById("display").unsafeSetInnerHTML(generateHtmlLines(resisting));
+  document.getElementById("test-css").unsafeSetInnerHTML(generateCSSLines(resisting));
   let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
   for (let div of cssTestDivs) {
     let color = window.getComputedStyle(div).backgroundColor;
     ok(color === green, "CSS for '" + div.id + "'");
   }
 };
 
 // __testOSXFontSmoothing(resisting)__.
@@ -279,17 +289,17 @@ var testMediaQueriesInPictureElements = 
     if (expected) {
       let query = constructQuery(key, expected);
       lines += "<picture>\n";
       lines += " <source srcset='/tests/layout/style/test/chrome/match.png' media='" + query + "' />\n";
       lines += " <img title='" + key + ":" + expected + "' class='testImage' src='/tests/layout/style/test/chrome/mismatch.png' alt='" + key + "' />\n";
       lines += "</picture><br/>\n";
     }
   }
-  document.getElementById("pictures").innerHTML = lines;
+  document.getElementById("pictures").unsafeSetInnerHTML(lines);
   var testImages = document.getElementsByClassName("testImage");
   await sleep(0);
   for (let testImage of testImages) {
     ok(testImage.currentSrc.endsWith("/match.png"), "Media query '" + testImage.title + "' in picture should match.");
   }
 };
 
 // __pushPref(key, value)__.
--- a/mobile/android/chrome/content/config.js
+++ b/mobile/android/chrome/content/config.js
@@ -592,18 +592,17 @@ Pref.prototype = {
         function(aEvent) {
           AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent);
         }
       );
 
       this.li.setAttribute("contextmenu", "prefs-context-menu");
 
       // Create list item outline, bind to object actions
-      // eslint-disable-next-line no-unsanitized/property
-      this.li.innerHTML =
+      this.li.unsafeSetInnerHTML(
         "<div class='pref-name' " +
             "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
             this.name +
         "</div>" +
         "<div class='pref-item-line'>" +
           "<input class='pref-value' value='' " +
             "onblur='AboutConfig.setIntOrStringPref(event);' " +
             "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
@@ -617,17 +616,17 @@ Pref.prototype = {
             gStringBundle.GetStringFromName("pref.toggleButton") +
           "</div>" +
           "<div class='pref-button up' " +
             "onclick='AboutConfig.incrOrDecrIntPref(event, 1);'>" +
           "</div>" +
           "<div class='pref-button down' " +
             "onclick='AboutConfig.incrOrDecrIntPref(event, -1);'>" +
           "</div>" +
-        "</div>";
+        "</div>");
 
       // Delay providing the list item values, until the LI is returned and added to the document
       setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
     }
 
     return this.li;
   },
 
--- a/toolkit/content/tests/chrome/test_bug570192.xul
+++ b/toolkit/content/tests/chrome/test_bug570192.xul
@@ -31,18 +31,18 @@ https://bugzilla.mozilla.org/show_bug.cg
   </body>
 
   <script type="application/javascript">
     <![CDATA[
 
     addLoadEvent(function() {
       try {
         var content = document.getElementById("content");
-        content.innerHTML = '<textbox newlines="pasteintact" ' +
-          'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>';
+        content.unsafeSetInnerHTML('<textbox newlines="pasteintact" ' +
+          'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>');
         var textbox = content.firstChild;
         ok(textbox, "created the textbox");
         ok(!textbox.editor, "do we have an editor?");
       } catch (e) {
         ok(false, "Got an exception: " + e);
       }
       SimpleTest.finish();
     });
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -3201,17 +3201,17 @@ var gDetailView = {
 
     var fullDesc = document.getElementById("detail-fulldesc");
     if (aAddon.fullDescription) {
       // The following is part of an awful hack to include the licenses for GMP
       // plugins without having bug 624602 fixed yet, and intentionally ignores
       // localisation.
       if (aAddon.isGMPlugin) {
         // eslint-disable-next-line no-unsanitized/property
-        fullDesc.innerHTML = aAddon.fullDescription;
+        fullDesc.unsafeSetInnerHTML(aAddon.fullDescription);
       } else {
         fullDesc.textContent = aAddon.fullDescription;
       }
 
       fullDesc.hidden = false;
     } else {
       fullDesc.hidden = true;
     }