Bug 1432966: Sanitize HTML fragments created for chrome-privileged documents. r?bz draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 24 Jan 2018 14:56:48 -0800
changeset 724521 d4154ae5f6f9ca90ef373198af60e40849ce0908
parent 724456 d22cc0f160121630d4a788d7c1fd8b8e88c4fbb0
push id96763
push usermaglione.k@gmail.com
push dateThu, 25 Jan 2018 04:20:47 +0000
reviewersbz
bugs1432966
milestone60.0a1
Bug 1432966: Sanitize HTML fragments created for chrome-privileged documents. r?bz 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
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
toolkit/content/tests/chrome/test_bug570192.xul
--- 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/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 chrom-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,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
@@ -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;
@@ -2329,24 +2330,26 @@ FragmentOrElement::SetInnerHTMLInternal(
   if (doc->IsHTMLDocument()) {
     int32_t oldChildCount = target->GetChildCount();
     aError = nsContentUtils::ParseFragmentHTML(aInnerHTML,
                                                target,
                                                contextLocalName,
                                                contextNameSpaceID,
                                                doc->GetCompatibilityMode() ==
                                                  eCompatibility_NavQuirks,
-                                               true);
+                                               true,
+                                               aNeverSanitize);
     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, aError,
+                                               aNeverSanitize);
     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,17 +4956,18 @@ nsContentUtils::CreateContextualFragment
                                       aPreventScriptExecution, rv).take();
   return rv.StealNSResult();
 }
 
 already_AddRefed<DocumentFragment>
 nsContentUtils::CreateContextualFragment(nsINode* aContextNode,
                                          const nsAString& aFragment,
                                          bool aPreventScriptExecution,
-                                         ErrorResult& aRv)
+                                         ErrorResult& aRv,
+                                         bool aNeverSanitize)
 {
   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
   // for compiling event handlers... so just bail out.
@@ -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,
+                              aNeverSanitize);
     } else {
       aRv = ParseFragmentHTML(aFragment, frag,
                               nsGkAtoms::body,
                               kNameSpaceID_XHTML,
                               (document->GetCompatibilityMode() ==
                                eCompatibility_NavQuirks),
-                              aPreventScriptExecution);
+                              aPreventScriptExecution,
+                              aNeverSanitize);
     }
 
     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),
+                         aNeverSanitize);
   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,
+                                  bool aNeverSanitize)
 {
   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 (!aNeverSanitize && !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,
+                                 bool aNeverSanitize)
 {
   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);
@@ -5182,16 +5216,29 @@ nsContentUtils::ParseFragmentXML(const n
     NS_IF_RELEASE(sXMLFragmentSink);
     return rv;
   }
 
   rv = sXMLFragmentSink->FinishFragmentParsing(aReturn);
 
   sXMLFragmentParser->Reset();
 
+  // If this is a chrome-privileged document, sanitize the fragment before
+  // returning.
+  if (!aNeverSanitize && !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,
                                    uint32_t aFlags,
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1594,17 +1594,18 @@ public:
    */
   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,
-                           mozilla::ErrorResult& aRv);
+                           mozilla::ErrorResult& aRv,
+                           bool aNeverSanitize = false);
 
   /**
    * 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
@@ -1616,34 +1617,36 @@ public:
    *         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,
+                                    bool aNeverSanitize = false);
 
   /**
    * 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
    * @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,
+                                   bool aNeverSanitize = false);
 
   /**
    * 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),
@@ -6182,16 +6183,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
@@ -2868,16 +2868,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.
@@ -3551,16 +3553,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
@@ -234,18 +234,18 @@ var green = (function () {
   return getComputedStyle(temp).backgroundColor;
 })();
 
 // __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 +279,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/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();
     });