Bug 1432966: Sanitize HTML fragments created for chrome-privileged documents. r=bz f=gijs a=jcristau DEVEDITION_59_0b5_BUILD1 DEVEDITION_59_0b5_RELEASE FENNEC_59_0b5_BUILD1 FENNEC_59_0b5_RELEASE FIREFOX_59_0b5_BUILD1 FIREFOX_59_0b5_RELEASE
authorKris Maglione <maglione.k@gmail.com>
Wed, 24 Jan 2018 14:56:48 -0800
changeset 454546 64737c752ac4af4766ad6f82720818521f3aca24
parent 454545 fa9e35ab29b4eeb344b9446ef21cb220dc966b52
child 454547 d64bb04cab4ae51f4936d7c88ff8acd6f15e2e2f
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jcristau
bugs1432966
milestone59.0
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/test/permissions/browser_reservedkey.js
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/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/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
@@ -644,31 +644,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 PropTypes = require("devtools/clie
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 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;
+
 class Browser extends PureComponent {
   /**
    * 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.
    */
   static get propTypes() {
--- 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,20 +493,22 @@ 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 {
+  } else {
     try {
       var ns = elem.ownerDocument.documentElement.namespaceURI;
       if (!ns) {
         ns = exports.NS_XHTML;
       }
       exports.clearElement(elem);
       contents = '<div xmlns="' + ns + '">' + contents + '</div>';
       var range = elem.ownerDocument.createRange();
--- 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
@@ -3923,16 +3923,22 @@ Element::GetInnerHTML(nsAString& aInnerH
 
 void
 Element::SetInnerHTML(const nsAString& aInnerHTML, nsIPrincipal* aSubjectPrincipal, 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
@@ -1413,16 +1413,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, nsIPrincipal* aSubjectPrincipal, 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
@@ -2269,17 +2269,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;
@@ -2321,32 +2322,37 @@ FragmentOrElement::SetInnerHTMLInternal(
   int32_t contextNameSpaceID = GetNameSpaceID();
 
   if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(this)) {
     // 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
@@ -327,17 +327,18 @@ public:
     /**
      * An object implementing the .classList property for this element.
      */
     RefPtr<nsDOMTokenList> mClassList;
   };
 
 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
   nsIContent::nsContentSlots* CreateSlots() override
   {
     return new nsDOMSlots();
   }
 
   nsIContent::nsExtendedContentSlots* CreateExtendedSlots() final override
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -157,16 +157,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"
@@ -195,16 +196,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"
@@ -4954,16 +4956,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
@@ -4989,24 +4992,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);
@@ -5060,17 +5065,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);
@@ -5087,37 +5093,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)
@@ -5142,17 +5175,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);
@@ -5181,16 +5215,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
@@ -1573,77 +1573,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
@@ -1478,16 +1478,17 @@ nsIDocument::nsIDocument()
     mFrameRequestCallbacksScheduled(false),
     mIsTopLevelContentDocument(false),
     mIsContentDocument(false),
     mDidCallBeginLoad(false),
     mBufferingCSPViolations(false),
     mAllowPaymentRequest(false),
     mEncodingMenuDisabled(false),
     mIsSVGGlyphsDocument(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),
@@ -6183,16 +6184,23 @@ nsIDocument::CreateAttributeNS(const nsA
     return nullptr;
   }
 
   RefPtr<Attr> attribute = new Attr(nullptr, nodeInfo.forget(),
                                     EmptyString());
   return attribute.forget();
 }
 
+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
@@ -2867,16 +2867,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.
@@ -3550,16 +3552,20 @@ protected:
 
   // True if dom.webcomponents.shadowdom.enabled pref is set when document is
   // created.
   bool mIsShadowDOMEnabled : 1;
 
   // True if this document is for an SVG-in-OpenType font.
   bool mIsSVGGlyphsDocument : 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
@@ -231,16 +231,26 @@ partial interface Element {
 // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface
 partial interface Element {
   [CEReactions, SetterNeedsSubjectPrincipal=NonSystem, 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
@@ -2781,17 +2781,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;
     }